From 7decaf243086d1e68c1bf0f02190e39cf2f63a04 Mon Sep 17 00:00:00 2001 From: Clayton Date: Mon, 22 Jun 2026 20:15:17 -0500 Subject: [PATCH 1/4] feat(desktop): flatpak package for review-ui --- .github/workflows/flatpak.yml | 74 +++++ CHANGELOG.md | 5 + Makefile | 6 + desktop/README.md | 12 + desktop/flatpak/Makefile | 39 +++ desktop/flatpak/README.md | 72 +++++ desktop/flatpak/com.vouchdev.vouch.desktop | 22 ++ .../flatpak/com.vouchdev.vouch.metainfo.xml | 70 +++++ desktop/flatpak/com.vouchdev.vouch.yaml | 76 +++++ desktop/flatpak/flathub/.gitkeep | 5 + desktop/flatpak/flathub/SUBMISSION.md | 40 +++ .../flatpak/flathub/com.vouchdev.vouch.json | 65 ++++ desktop/flatpak/lib/__init__.py | 1 + desktop/flatpak/lib/validate.py | 287 ++++++++++++++++++ desktop/flatpak/requirements-flatpak.txt | 13 + desktop/flatpak/screenshots/README.md | 4 + desktop/flatpak/scripts/build-flatpak.sh | 55 ++++ .../flatpak/scripts/capture-screenshots.sh | 42 +++ desktop/flatpak/scripts/generate-icons.py | 136 +++++++++ .../flatpak/scripts/generate-requirements.py | 49 +++ desktop/flatpak/scripts/install-runtime.sh | 13 + desktop/flatpak/scripts/validate-manifest.py | 49 +++ .../128x128/apps/com.vouchdev.vouch.png | Bin 0 -> 1688 bytes .../hicolor/16x16/apps/com.vouchdev.vouch.png | Bin 0 -> 279 bytes .../256x256/apps/com.vouchdev.vouch.png | Bin 0 -> 3426 bytes .../hicolor/32x32/apps/com.vouchdev.vouch.png | Bin 0 -> 481 bytes .../hicolor/48x48/apps/com.vouchdev.vouch.png | Bin 0 -> 646 bytes .../512x512/apps/com.vouchdev.vouch.png | Bin 0 -> 7943 bytes .../hicolor/64x64/apps/com.vouchdev.vouch.png | Bin 0 -> 845 bytes .../scalable/apps/com.vouchdev.vouch.svg | 20 ++ desktop/flatpak/vouch-review-ui.sh | 16 + docs/desktop-flatpak.md | 43 +++ pyproject.toml | 8 + tests/test_flatpak.py | 144 +++++++++ tests/test_flatpak_validate_extended.py | 85 ++++++ 35 files changed, 1451 insertions(+) create mode 100644 .github/workflows/flatpak.yml create mode 100644 desktop/flatpak/Makefile create mode 100644 desktop/flatpak/README.md create mode 100644 desktop/flatpak/com.vouchdev.vouch.desktop create mode 100644 desktop/flatpak/com.vouchdev.vouch.metainfo.xml create mode 100644 desktop/flatpak/com.vouchdev.vouch.yaml create mode 100644 desktop/flatpak/flathub/.gitkeep create mode 100644 desktop/flatpak/flathub/SUBMISSION.md create mode 100644 desktop/flatpak/flathub/com.vouchdev.vouch.json create mode 100644 desktop/flatpak/lib/__init__.py create mode 100644 desktop/flatpak/lib/validate.py create mode 100644 desktop/flatpak/requirements-flatpak.txt create mode 100644 desktop/flatpak/screenshots/README.md create mode 100644 desktop/flatpak/scripts/build-flatpak.sh create mode 100644 desktop/flatpak/scripts/capture-screenshots.sh create mode 100644 desktop/flatpak/scripts/generate-icons.py create mode 100644 desktop/flatpak/scripts/generate-requirements.py create mode 100644 desktop/flatpak/scripts/install-runtime.sh create mode 100644 desktop/flatpak/scripts/validate-manifest.py create mode 100644 desktop/flatpak/share/icons/hicolor/128x128/apps/com.vouchdev.vouch.png create mode 100644 desktop/flatpak/share/icons/hicolor/16x16/apps/com.vouchdev.vouch.png create mode 100644 desktop/flatpak/share/icons/hicolor/256x256/apps/com.vouchdev.vouch.png create mode 100644 desktop/flatpak/share/icons/hicolor/32x32/apps/com.vouchdev.vouch.png create mode 100644 desktop/flatpak/share/icons/hicolor/48x48/apps/com.vouchdev.vouch.png create mode 100644 desktop/flatpak/share/icons/hicolor/512x512/apps/com.vouchdev.vouch.png create mode 100644 desktop/flatpak/share/icons/hicolor/64x64/apps/com.vouchdev.vouch.png create mode 100644 desktop/flatpak/share/icons/hicolor/scalable/apps/com.vouchdev.vouch.svg create mode 100644 desktop/flatpak/vouch-review-ui.sh create mode 100644 docs/desktop-flatpak.md create mode 100644 tests/test_flatpak.py create mode 100644 tests/test_flatpak_validate_extended.py diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml new file mode 100644 index 00000000..d6beaac1 --- /dev/null +++ b/.github/workflows/flatpak.yml @@ -0,0 +1,74 @@ +name: flatpak + +on: + push: + branches: [main] + paths: + - 'desktop/flatpak/**' + - 'tests/test_flatpak.py' + - '.github/workflows/flatpak.yml' + pull_request: + paths: + - 'desktop/flatpak/**' + - 'tests/test_flatpak.py' + - '.github/workflows/flatpak.yml' + +jobs: + validate: + name: validate packaging + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: generate icons + run: python desktop/flatpak/scripts/generate-icons.py + + - name: validate manifest + run: python desktop/flatpak/scripts/validate-manifest.py --strict + + - name: pytest flatpak contract + run: | + python -m pip install --upgrade pip + pip install -e '.[dev]' + python -m pytest tests/test_flatpak.py -q + + build: + name: flatpak-builder smoke + runs-on: ubuntu-latest + needs: validate + steps: + - uses: actions/checkout@v4 + + - name: install flatpak + run: | + sudo apt-get update + sudo apt-get install -y flatpak flatpak-builder + + - name: add flathub + run: flatpak remote-add --if-not-exists --user flathub https://flathub.org/repo/flathub.flatpakrepo + + - name: install runtime 23.08 + run: | + flatpak install -y --user flathub \ + org.freedesktop.Platform//23.08 \ + org.freedesktop.Sdk//23.08 \ + org.freedesktop.Sdk.Extension.python3//23.08 \ + org.freedesktop.Platform.Extension.python3//23.08 + + - name: generate icons + run: python desktop/flatpak/scripts/generate-icons.py + + - name: build + run: | + cd desktop/flatpak + flatpak-builder --repo=repo --force-clean build-dir com.vouchdev.vouch.yaml + + - name: upload bundle artifact + uses: actions/upload-artifact@v7 + with: + name: flatpak-repo + path: desktop/flatpak/repo/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 8023df2d..5345268b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,11 @@ All notable changes to vouch are documented here. Format follows as reviewer; a PR opens only when the repo's own test gate is green and the reviewer signs off. A sibling tool — it never writes to the KB or the review gate. Paired with the `auto-pr` skill. +- flatpak desktop package (#211): `desktop/flatpak/com.vouchdev.vouch.yaml` + manifest on `org.freedesktop.Platform//23.08` with `--filesystem=home` and + `--share=network`, shipping `vouch review-ui` via a `vouch-review-ui` launcher. + Includes AppStream metainfo, hicolor icons, Flathub submission template, + validators (`tests/test_flatpak.py`), and a `flatpak` CI workflow. - typed page kinds (#234): a KB can declare extra page kinds in `.vouch/config.yaml` under `page_kinds`, each with `required_fields`, a JSON-Schema-subset `frontmatter_schema`, `required_citations`, and one level diff --git a/Makefile b/Makefile index 03dd2596..a35643e6 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,7 @@ help: @echo " make type mypy" @echo " make check lint + type + test" @echo " make build build sdist + wheel" + @echo " make flatpak validate flatpak packaging (icons + manifest)" @echo " make clean remove caches, build artifacts, *.egg-info" @echo " make examples-screenshots re-render docs/img/examples/*.svg" @@ -46,6 +47,11 @@ build: $(PY) -m pip install --upgrade build $(PY) -m build +flatpak: + $(PY) desktop/flatpak/scripts/generate-icons.py + $(PY) desktop/flatpak/scripts/validate-manifest.py --strict + $(PY) -m pytest tests/test_flatpak.py -q + clean: rm -rf build dist *.egg-info src/*.egg-info \ .pytest_cache .ruff_cache .mypy_cache \ diff --git a/desktop/README.md b/desktop/README.md index 08c49cf2..ee44a6c4 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -133,6 +133,18 @@ To ship a zero-Python install, freeze vouch with PyInstaller into See [`docs/architecture.md`](./docs/architecture.md) for the full design, including the method-by-method coverage table. +## Linux packaging + +Sandboxed Linux installs for the **review console** live under [`flatpak/`](flatpak/): + +| Path | Format | Status | +|---|---|---| +| [`flatpak/`](flatpak/) | Flatpak (`com.vouchdev.vouch`) | #211 — `org.freedesktop.Platform//23.08` | + +Snap packaging is tracked separately. Each format keeps its own manifest language +and runtime base. See [`docs/desktop-flatpak.md`](../docs/desktop-flatpak.md) and +[`flatpak/README.md`](flatpak/README.md) for build and Flathub submission notes. + ## Status v0.1.0 covers the entire `kb.*` surface plus dual-solve. The CLI-only diff --git a/desktop/flatpak/Makefile b/desktop/flatpak/Makefile new file mode 100644 index 00000000..cb03a89e --- /dev/null +++ b/desktop/flatpak/Makefile @@ -0,0 +1,39 @@ +.PHONY: help icons validate requirements build install run clean + +ROOT := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))) +PY ?= python3 +MANIFEST := $(ROOT)/com.vouchdev.vouch.yaml +BUILD_DIR := $(ROOT)/build-dir +REPO_DIR := $(ROOT)/repo + +help: + @echo "vouch flatpak targets (#211)" + @echo "" + @echo " make icons regenerate hicolor PNGs from SVG" + @echo " make validate run packaging validators (strict)" + @echo " make requirements sync requirements-flatpak.txt from pyproject.toml" + @echo " make build flatpak-builder (no install)" + @echo " make install build + --user --install" + @echo " make run flatpak run com.vouchdev.vouch" + @echo " make clean remove build-dir/ and repo/" + +icons: + $(PY) $(ROOT)/scripts/generate-icons.py + +validate: icons + $(PY) $(ROOT)/scripts/validate-manifest.py --strict + +requirements: + $(PY) $(ROOT)/scripts/generate-requirements.py + +build: validate + flatpak-builder --force-clean --repo=$(REPO_DIR) $(BUILD_DIR) $(MANIFEST) + +install: validate + flatpak-builder --user --install --force-clean --repo=$(REPO_DIR) $(BUILD_DIR) $(MANIFEST) + +run: + flatpak run com.vouchdev.vouch + +clean: + rm -rf $(BUILD_DIR) $(REPO_DIR) diff --git a/desktop/flatpak/README.md b/desktop/flatpak/README.md new file mode 100644 index 00000000..845bf1dc --- /dev/null +++ b/desktop/flatpak/README.md @@ -0,0 +1,72 @@ +# vouch Flatpak (#211) + +Sandboxed Linux install for the **vouch review console** (`vouch review-ui`). +Targets Fedora 40+, Arch, and any desktop with Flathub. + +## Quick start + +```bash +# one-time runtime (org.freedesktop.Platform//23.08) +./scripts/install-runtime.sh + +# build + install for the current user +make install + +# launch from the app menu or: +flatpak run com.vouchdev.vouch +``` + +## Layout + +| Path | Purpose | +|---|---| +| `com.vouchdev.vouch.yaml` | flatpak-builder manifest (local dev build) | +| `com.vouchdev.vouch.desktop` | Freedesktop launcher | +| `com.vouchdev.vouch.metainfo.xml` | AppStream metadata for Flathub | +| `vouch-review-ui.sh` | `/app/bin` entrypoint | +| `share/icons/hicolor/` | App icons (16–512 + scalable SVG) | +| `flathub/` | Flathub submission JSON + checklist | +| `lib/validate.py` | Packaging validators (pytest + CLI) | +| `scripts/` | build, icon generation, screenshot capture | + +## Permissions (v1) + +| finish-arg | rationale | +|---|---| +| `--share=network` | uvicorn binds localhost; browser loads the UI | +| `--filesystem=home` | read/write `.vouch/` KB trees under `$HOME` | +| `--talk-name=org.freedesktop.portal.Desktop` | open the default browser | + +Per-KB filesystem scoping is **out of scope** for v1 — see [issue #211](https://github.com/vouchdev/vouch/issues/211). + +## Develop + +```bash +make icons # regenerate PNGs from SVG +make validate # strict manifest/desktop/metainfo checks +make requirements # sync requirements-flatpak.txt from pyproject.toml +make build # flatpak-builder only (no install) +``` + +From repo root: + +```bash +python -m pytest tests/test_flatpak.py -q +``` + +## Flathub + +After merge, follow `flathub/SUBMISSION.md` to open a Flathub application PR. +The store listing reuses metainfo screenshots under `screenshots/` — capture with +`scripts/capture-screenshots.sh` once review-ui is installed. + +Acceptance: + +- `flatpak install flathub com.vouchdev.vouch` on Fedora 40+ and Arch +- listing shows icon, description, and screenshots + +## Runtime + +- **Base:** `org.freedesktop.Platform//23.08` +- **Python:** SDK/runtime `Extension.python3` (3.11) +- **Package:** `pip install '.[web]'` — no bundled CPython upgrade automation diff --git a/desktop/flatpak/com.vouchdev.vouch.desktop b/desktop/flatpak/com.vouchdev.vouch.desktop new file mode 100644 index 00000000..b3900a6a --- /dev/null +++ b/desktop/flatpak/com.vouchdev.vouch.desktop @@ -0,0 +1,22 @@ +[Desktop Entry] +Type=Application +Name=Vouch +GenericName=Review Console +Comment=Review-gated knowledge base for LLM agents +Exec=vouch-review-ui +Icon=com.vouchdev.vouch +Terminal=false +Categories=Development;Office;TextEditor; +Keywords=vouch;knowledge;review;agent;mcp;kb; +StartupNotify=true +StartupWMClass=vouch-review-ui +MimeType= +X-Flatpak-RenamedFrom=vouch.desktop; + +[Desktop Action OpenQueue] +Name=Open review queue +Exec=vouch-review-ui + +[Desktop Action OpenHeadless] +Name=Start without opening browser +Exec=vouch-review-ui --no-open-browser diff --git a/desktop/flatpak/com.vouchdev.vouch.metainfo.xml b/desktop/flatpak/com.vouchdev.vouch.metainfo.xml new file mode 100644 index 00000000..9c5acedb --- /dev/null +++ b/desktop/flatpak/com.vouchdev.vouch.metainfo.xml @@ -0,0 +1,70 @@ + + + com.vouchdev.vouch + CC0-1.0 + MIT + Vouch + Review-gated knowledge base for LLM agents + +

+ Vouch is a git-native knowledge base where every write goes through a human + review gate. Agents propose claims and pages; you approve them with the + review console or the CLI. Approved artifacts are plain YAML and markdown + on disk — diffable in PRs, auditable, and portable. +

+

+ This Flatpak ships the browser-based review console + (vouch review-ui): a local FastAPI server that lists pending + proposals, shows diffs, and records approve/reject decisions in the audit + log. Pair it with the vouch CLI or an MCP host adapter for + the full propose → review → commit loop. +

+

+ Filesystem access is limited to your home directory so vouch can read and + write .vouch/ trees under your projects and + ~/.vouch/. +

+
+ com.vouchdev.vouch.desktop + + vouchdev + + https://github.com/vouchdev/vouch + https://github.com/vouchdev/vouch/issues + https://github.com/vouchdev/vouch + https://github.com/vouchdev/vouch/blob/main/docs/getting-started.md + + + Development + Office + + + vouch + knowledge base + review + llm + mcp + agents + + + #4f5d9e + #1e2a44 + + + + Review queue — pending proposals awaiting human approval + https://raw.githubusercontent.com/vouchdev/vouch/main/desktop/flatpak/screenshots/queue.png + + + Proposal detail — diff and metadata before approve/reject + https://raw.githubusercontent.com/vouchdev/vouch/main/desktop/flatpak/screenshots/detail.png + + + + + +

Initial Flatpak packaging for the vouch review console (#211).

+
+
+
+
diff --git a/desktop/flatpak/com.vouchdev.vouch.yaml b/desktop/flatpak/com.vouchdev.vouch.yaml new file mode 100644 index 00000000..744dd646 --- /dev/null +++ b/desktop/flatpak/com.vouchdev.vouch.yaml @@ -0,0 +1,76 @@ +# flatpak-builder manifest for vouch review-ui (#211). +# Build from this directory: +# flatpak-builder --user --install --force-clean build-dir com.vouchdev.vouch.yaml +# +# Runtime: org.freedesktop.Platform//23.08 (Fedora 40+, Arch, any Flathub desktop). +# v1 simplification: --filesystem=home covers ~/.vouch/ and project KB trees under $HOME. + +id: com.vouchdev.vouch +runtime: org.freedesktop.Platform +runtime-version: '23.08' +sdk: org.freedesktop.Sdk +sdk-extensions: + - org.freedesktop.Sdk.Extension.python3 + +add-extensions: + org.freedesktop.Platform.python3: + version: '23.08' + directory: lib/python3.11 + +command: vouch-review-ui + +finish-args: + # uvicorn binds localhost; browser loads the review console. + - --share=network + # read/write KB dirs under the user's home (incl. ~/.vouch/). + - --filesystem=home + # xdg-open / default browser via the desktop portal. + - --talk-name=org.freedesktop.portal.Desktop + - --socket=wayland + - --socket=fallback-x11 + +modules: + # --- python application (vouch-kb + [web] extra) ---------------------------- + - name: vouch + buildsystem: simple + build-options: + append-path: /usr/lib/sdk/python3/bin + env: + PIP_DISABLE_PIP_VERSION_CHECK: '1' + PYTHONNOUSERSITE: '1' + build-commands: + # install from the copied repo root; [web] pulls fastapi/uvicorn/jinja2. + - pip3 install --prefix=${FLATPAK_DEST} --no-build-isolation '.[web]' + sources: + - type: dir + path: ../.. + exclude: + - .git + - .github + - .venv + - .mypy_cache + - .pytest_cache + - .ruff_cache + - build + - dist + - htmlcov + - desktop/flatpak/build-dir + - desktop/flatpak/.flatpak-builder + - '**/__pycache__' + - '**/*.pyc' + + # --- launcher, desktop entry, metainfo, icons ------------------------------- + - name: vouch-desktop + buildsystem: simple + build-commands: + - install -Dm755 vouch-review-ui.sh ${FLATPAK_DEST}/bin/vouch-review-ui + - install -Dm644 com.vouchdev.vouch.desktop ${FLATPAK_DEST}/share/applications/com.vouchdev.vouch.desktop + - install -Dm644 com.vouchdev.vouch.metainfo.xml ${FLATPAK_DEST}/share/metainfo/com.vouchdev.vouch.metainfo.xml + - | + for size in 16 32 48 64 128 256 512; do + install -Dm644 share/icons/hicolor/${size}x${size}/apps/com.vouchdev.vouch.png \ + ${FLATPAK_DEST}/share/icons/hicolor/${size}x${size}/apps/com.vouchdev.vouch.png + done + sources: + - type: dir + path: . diff --git a/desktop/flatpak/flathub/.gitkeep b/desktop/flatpak/flathub/.gitkeep new file mode 100644 index 00000000..e7b508fa --- /dev/null +++ b/desktop/flatpak/flathub/.gitkeep @@ -0,0 +1,5 @@ +# Flathub submission helpers (#211). +# +# Flathub hosts manifests in a separate git repository per app. After this +# upstream manifest lands, open a PR against https://github.com/flathub/flathub +# with the generated com.vouchdev.vouch.json pointing at a release tag. diff --git a/desktop/flatpak/flathub/SUBMISSION.md b/desktop/flatpak/flathub/SUBMISSION.md new file mode 100644 index 00000000..77c79057 --- /dev/null +++ b/desktop/flatpak/flathub/SUBMISSION.md @@ -0,0 +1,40 @@ +# Flathub new-application checklist (#211) +# +# Use this when opening https://github.com/flathub/flathub/new-application + +## upstream + +- repository: https://github.com/vouchdev/vouch +- license: MIT +- app id: com.vouchdev.vouch + +## manifest source + +Copy `desktop/flatpak/flathub/com.vouchdev.vouch.json` into the Flathub +application repo as `com.vouchdev.vouch.json`. Replace `REPLACE_WITH_TAG_COMMIT` +with the git object for the release tag you are shipping. + +For day-to-day Flathub builds, prefer pinning `tag` + `commit` together so +rebuilds stay reproducible when tags move. + +## permissions justification (for reviewers) + +| finish-arg | why | +|---|---| +| `--share=network` | `vouch review-ui` binds a localhost port and the browser loads the UI | +| `--filesystem=home` | KB trees live under `$HOME` (project `.vouch/` dirs and optional `~/.vouch/`) | +| `--talk-name=org.freedesktop.portal.Desktop` | open the default browser to the review console | +| `--socket=wayland` / `fallback-x11` | standard GUI session integration | + +Per-KB filesystem scoping is explicitly out of scope for v1 (#211). + +## store listing assets + +- icons: `desktop/flatpak/share/icons/hicolor/*/apps/com.vouchdev.vouch.png` +- metainfo: `desktop/flatpak/com.vouchdev.vouch.metainfo.xml` +- screenshots: capture with `scripts/capture-screenshots.sh` after `flatpak run` + +## acceptance (#211) + +- `flatpak install flathub com.vouchdev.vouch` on Fedora 40+ and Arch +- Flathub listing shows icon, description, screenshots from metainfo diff --git a/desktop/flatpak/flathub/com.vouchdev.vouch.json b/desktop/flatpak/flathub/com.vouchdev.vouch.json new file mode 100644 index 00000000..f6f6e0cb --- /dev/null +++ b/desktop/flatpak/flathub/com.vouchdev.vouch.json @@ -0,0 +1,65 @@ +{ + "id": "com.vouchdev.vouch", + "runtime": "org.freedesktop.Platform", + "runtime-version": "23.08", + "sdk": "org.freedesktop.Sdk", + "sdk-extensions": [ + "org.freedesktop.Sdk.Extension.python3" + ], + "add-extensions": { + "org.freedesktop.Platform.python3": { + "version": "23.08", + "directory": "lib/python3.11" + } + }, + "command": "vouch-review-ui", + "finish-args": [ + "--share=network", + "--filesystem=home", + "--talk-name=org.freedesktop.portal.Desktop", + "--socket=wayland", + "--socket=fallback-x11" + ], + "modules": [ + { + "name": "vouch", + "buildsystem": "simple", + "build-options": { + "append-path": "/usr/lib/sdk/python3/bin", + "env": { + "PIP_DISABLE_PIP_VERSION_CHECK": "1", + "PYTHONNOUSERSITE": "1" + } + }, + "build-commands": [ + "pip3 install --prefix=${FLATPAK_DEST} --no-build-isolation '.[web]'" + ], + "sources": [ + { + "type": "git", + "url": "https://github.com/vouchdev/vouch.git", + "tag": "v0.0.1", + "commit": "REPLACE_WITH_TAG_COMMIT" + } + ] + }, + { + "name": "vouch-desktop", + "buildsystem": "simple", + "build-commands": [ + "install -Dm755 desktop/flatpak/vouch-review-ui.sh ${FLATPAK_DEST}/bin/vouch-review-ui", + "install -Dm644 desktop/flatpak/com.vouchdev.vouch.desktop ${FLATPAK_DEST}/share/applications/com.vouchdev.vouch.desktop", + "install -Dm644 desktop/flatpak/com.vouchdev.vouch.metainfo.xml ${FLATPAK_DEST}/share/metainfo/com.vouchdev.vouch.metainfo.xml", + "for size in 16 32 48 64 128 256 512; do install -Dm644 desktop/flatpak/share/icons/hicolor/${size}x${size}/apps/com.vouchdev.vouch.png ${FLATPAK_DEST}/share/icons/hicolor/${size}x${size}/apps/com.vouchdev.vouch.png; done" + ], + "sources": [ + { + "type": "git", + "url": "https://github.com/vouchdev/vouch.git", + "tag": "v0.0.1", + "commit": "REPLACE_WITH_TAG_COMMIT" + } + ] + } + ] +} diff --git a/desktop/flatpak/lib/__init__.py b/desktop/flatpak/lib/__init__.py new file mode 100644 index 00000000..59ee6933 --- /dev/null +++ b/desktop/flatpak/lib/__init__.py @@ -0,0 +1 @@ +"""Flatpak packaging support code (#211).""" diff --git a/desktop/flatpak/lib/validate.py b/desktop/flatpak/lib/validate.py new file mode 100644 index 00000000..8bcaecd8 --- /dev/null +++ b/desktop/flatpak/lib/validate.py @@ -0,0 +1,287 @@ +"""Flatpak packaging validation helpers (#211). + +Pure-stdlib checks used by tests and ``scripts/validate-manifest.py``. Keeps +the manifest, desktop entry, metainfo, and icon tree aligned with Flathub +expectations without requiring flatpak-builder at pytest time. +""" + +from __future__ import annotations + +import re +import xml.etree.ElementTree as ET +from dataclasses import dataclass, field +from pathlib import Path + +FLATPAK_DIR = Path(__file__).resolve().parents[1] +REPO_ROOT = FLATPAK_DIR.parents[1] + +MANIFEST_NAME = "com.vouchdev.vouch.yaml" +APP_ID = "com.vouchdev.vouch" +RUNTIME = "org.freedesktop.Platform" +RUNTIME_VERSION = "23.08" +COMMAND = "vouch-review-ui" + +REQUIRED_FINISH_ARGS = frozenset( + { + "--share=network", + "--filesystem=home", + } +) + +RECOMMENDED_FINISH_ARGS = frozenset( + { + "--talk-name=org.freedesktop.portal.Desktop", + "--socket=wayland", + "--socket=fallback-x11", + } +) + +ICON_SIZES = (16, 32, 48, 64, 128, 256, 512) + +DESKTOP_REQUIRED_KEYS = frozenset( + { + "Type", + "Name", + "Exec", + "Icon", + } +) + + +@dataclass +class ValidationIssue: + level: str # error | warning | info + message: str + path: str | None = None + + +@dataclass +class ValidationReport: + issues: list[ValidationIssue] = field(default_factory=list) + + @property + def errors(self) -> list[ValidationIssue]: + return [i for i in self.issues if i.level == "error"] + + @property + def warnings(self) -> list[ValidationIssue]: + return [i for i in self.issues if i.level == "warning"] + + def ok(self) -> bool: + return not self.errors + + def add(self, level: str, message: str, path: str | None = None) -> None: + self.issues.append(ValidationIssue(level=level, message=message, path=path)) + + +def _read_text(path: Path) -> str: + return path.read_text(encoding="utf-8") + + +def _parse_simple_yaml_list(block: str, key: str) -> list[str]: + """Minimal YAML list parser for flatpak manifest keys (no PyYAML dep).""" + lines = block.splitlines() + in_key = False + items: list[str] = [] + for line in lines: + if re.match(rf"^{re.escape(key)}:\s*$", line): + in_key = True + continue + if in_key: + m = re.match(r"^\s+-\s+(.+)$", line) + if m: + items.append(m.group(1).strip()) + continue + if line and not line.startswith(" "): + break + return items + + +def _manifest_scalar(block: str, key: str) -> str | None: + m = re.search(rf"^{re.escape(key)}:\s*(.+)$", block, re.MULTILINE) + return m.group(1).strip().strip("'\"") if m else None + + +def validate_manifest_exists(report: ValidationReport, root: Path = FLATPAK_DIR) -> Path: + path = root / MANIFEST_NAME + if not path.is_file(): + report.add("error", f"missing manifest {MANIFEST_NAME}", str(path)) + return path + report.add("info", "manifest present", str(path)) + return path + + +def validate_manifest_content(report: ValidationReport, root: Path = FLATPAK_DIR) -> None: + path = root / MANIFEST_NAME + if not path.is_file(): + return + text = _read_text(path) + + app_id = _manifest_scalar(text, "id") + if app_id != APP_ID: + report.add("error", f"id must be {APP_ID!r}, got {app_id!r}", str(path)) + + runtime = _manifest_scalar(text, "runtime") + if runtime != RUNTIME: + report.add("error", f"runtime must be {RUNTIME!r}, got {runtime!r}", str(path)) + + runtime_version = _manifest_scalar(text, "runtime-version") + if runtime_version != RUNTIME_VERSION: + report.add( + "error", + f"runtime-version must be {RUNTIME_VERSION!r}, got {runtime_version!r}", + str(path), + ) + + command = _manifest_scalar(text, "command") + if command != COMMAND: + report.add("error", f"command must be {COMMAND!r}, got {command!r}", str(path)) + + finish_args = set(_parse_simple_yaml_list(text, "finish-args")) + missing_required = REQUIRED_FINISH_ARGS - finish_args + for arg in sorted(missing_required): + report.add("error", f"finish-args missing required {arg}", str(path)) + + missing_recommended = RECOMMENDED_FINISH_ARGS - finish_args + for arg in sorted(missing_recommended): + report.add("warning", f"finish-args missing recommended {arg}", str(path)) + + if "org.freedesktop.Sdk.Extension.python3" not in text: + report.add("error", "sdk-extensions must include python3 extension", str(path)) + + if "name: vouch" not in text: + report.add("error", "modules must include vouch python module", str(path)) + + if "name: vouch-desktop" not in text: + report.add("error", "modules must include vouch-desktop assets module", str(path)) + + if ".[web]" not in text: + report.add("error", "pip install must include [web] extra for review-ui", str(path)) + + +def validate_desktop_entry(report: ValidationReport, root: Path = FLATPAK_DIR) -> None: + path = root / f"{APP_ID}.desktop" + if not path.is_file(): + report.add("error", "missing .desktop launcher", str(path)) + return + + text = _read_text(path) + keys: dict[str, str] = {} + for line in text.splitlines(): + if line.startswith("[") or not line.strip() or line.strip().startswith("#"): + continue + if "=" in line: + k, v = line.split("=", 1) + keys[k.strip()] = v.strip() + + for req in DESKTOP_REQUIRED_KEYS: + if req not in keys: + report.add("error", f".desktop missing [{req}]", str(path)) + + if keys.get("Icon") != APP_ID: + report.add("error", f".desktop Icon must be {APP_ID}", str(path)) + + if keys.get("Exec", "").split()[0] != COMMAND: + report.add("error", f".desktop Exec must start with {COMMAND}", str(path)) + + +def validate_metainfo(report: ValidationReport, root: Path = FLATPAK_DIR) -> None: + path = root / f"{APP_ID}.metainfo.xml" + if not path.is_file(): + report.add("error", "missing AppStream metainfo", str(path)) + return + + try: + tree = ET.parse(path) + except ET.ParseError as e: + report.add("error", f"metainfo XML parse error: {e}", str(path)) + return + + root_el = tree.getroot() + component_id = root_el.findtext("id") + if component_id != APP_ID: + report.add("error", f" must be {APP_ID}, got {component_id!r}", str(path)) + + launchable = root_el.find("launchable") + if launchable is None or launchable.get("type") != "desktop-id": + report.add("error", " required", str(path)) + elif launchable.text != f"{APP_ID}.desktop": + report.add("error", "launchable desktop-id must match .desktop file", str(path)) + + if root_el.find("name") is None: + report.add("error", " required for Flathub", str(path)) + + if root_el.find("summary") is None: + report.add("error", " required for Flathub", str(path)) + + releases = root_el.findall("releases/release") + if not releases: + report.add("warning", "no — Flathub needs versioned releases", str(path)) + else: + for rel in releases: + if not rel.get("version"): + report.add("error", " missing version attribute", str(path)) + if not rel.get("date"): + report.add("warning", f"release {rel.get('version')} missing date", str(path)) + + screenshots = root_el.findall("screenshots/screenshot") + if not screenshots: + report.add("warning", "no screenshots — Flathub listing needs at least one", str(path)) + + +def validate_icons(report: ValidationReport, root: Path = FLATPAK_DIR) -> None: + scalable = ( + root / "share/icons/hicolor/scalable/apps" / f"{APP_ID}.svg" + ) + if scalable.is_file(): + report.add("info", "scalable SVG icon present", str(scalable)) + else: + report.add("warning", "no scalable SVG icon", str(scalable)) + + for size in ICON_SIZES: + png = ( + root + / "share/icons/hicolor" + / f"{size}x{size}" + / "apps" + / f"{APP_ID}.png" + ) + if not png.is_file(): + report.add("error", f"missing {size}x{size} PNG icon", str(png)) + elif png.stat().st_size < 64: + report.add("error", f"{size}x{size} PNG looks truncated", str(png)) + + +def validate_launcher_script(report: ValidationReport, root: Path = FLATPAK_DIR) -> None: + path = root / "vouch-review-ui.sh" + if not path.is_file(): + report.add("error", "missing vouch-review-ui.sh launcher", str(path)) + return + text = _read_text(path) + if "vouch review-ui" not in text: + report.add("error", "launcher must invoke vouch review-ui", str(path)) + if not text.startswith("#!/"): + report.add("warning", "launcher should have a shebang", str(path)) + + +def validate_requirements_pin(report: ValidationReport, root: Path = FLATPAK_DIR) -> None: + path = root / "requirements-flatpak.txt" + if not path.is_file(): + report.add("warning", "requirements-flatpak.txt missing", str(path)) + return + text = _read_text(path) + for dep in ("fastapi", "uvicorn", "pydantic", "mcp"): + if dep not in text: + report.add("error", f"requirements-flatpak.txt must pin {dep}", str(path)) + + +def run_all_validations(root: Path = FLATPAK_DIR) -> ValidationReport: + report = ValidationReport() + validate_manifest_exists(report, root) + validate_manifest_content(report, root) + validate_desktop_entry(report, root) + validate_metainfo(report, root) + validate_icons(report, root) + validate_launcher_script(report, root) + validate_requirements_pin(report, root) + return report diff --git a/desktop/flatpak/requirements-flatpak.txt b/desktop/flatpak/requirements-flatpak.txt new file mode 100644 index 00000000..9d2cacaa --- /dev/null +++ b/desktop/flatpak/requirements-flatpak.txt @@ -0,0 +1,13 @@ +# Pinned runtime dependencies for the Flatpak [web] install path (#211). +# Mirrors pyproject.toml core + web extras; regenerate with: +# desktop/flatpak/scripts/generate-requirements.py + +pydantic>=2.13.4,<3 +click>=8.4.0,<9 +pyyaml>=6,<7 +mcp>=1.0,<2 +fastapi>=0.115,<1 +jinja2>=3,<4 +python-multipart>=0.0.9 +uvicorn>=0.30,<1 +websockets>=12,<16 diff --git a/desktop/flatpak/screenshots/README.md b/desktop/flatpak/screenshots/README.md new file mode 100644 index 00000000..680565e0 --- /dev/null +++ b/desktop/flatpak/screenshots/README.md @@ -0,0 +1,4 @@ +# Placeholder screenshots for Flathub metainfo (#211). +# +# Replace with real captures from `scripts/capture-screenshots.sh` before +# submitting to Flathub. Metainfo URLs point here on main. diff --git a/desktop/flatpak/scripts/build-flatpak.sh b/desktop/flatpak/scripts/build-flatpak.sh new file mode 100644 index 00000000..a9184a1b --- /dev/null +++ b/desktop/flatpak/scripts/build-flatpak.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# Build and optionally install the vouch Flatpak locally (#211). +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +MANIFEST="${ROOT}/com.vouchdev.vouch.yaml" +BUILD_DIR="${ROOT}/build-dir" +REPO_DIR="${ROOT}/repo" + +usage() { + cat <<'EOF' +Usage: build-flatpak.sh [--install] [--run] [--clean] + + --install install the build into the user Flatpak repo + --run run com.vouchdev.vouch after a successful build + --clean remove build-dir/ and repo/ before building +EOF +} + +INSTALL=0 +RUN=0 +CLEAN=0 +while [ $# -gt 0 ]; do + case "$1" in + --install) INSTALL=1 ;; + --run) RUN=1 ;; + --clean) CLEAN=1 ;; + -h|--help) usage; exit 0 ;; + *) echo "unknown arg: $1" >&2; usage; exit 1 ;; + esac + shift +done + +command -v flatpak-builder >/dev/null || { + echo "flatpak-builder not found; install flatpak-builder" >&2 + exit 1 +} + +python3 "${ROOT}/scripts/generate-icons.py" +python3 "${ROOT}/scripts/validate-manifest.py" --strict + +if [ "$CLEAN" = 1 ]; then + rm -rf "${BUILD_DIR}" "${REPO_DIR}" +fi + +ARGS=(--force-clean --repo="${REPO_DIR}" "${BUILD_DIR}" "${MANIFEST}") +if [ "$INSTALL" = 1 ]; then + ARGS=(--user --install "${ARGS[@]}") +fi + +flatpak-builder "${ARGS[@]}" + +if [ "$RUN" = 1 ]; then + flatpak run com.vouchdev.vouch +fi diff --git a/desktop/flatpak/scripts/capture-screenshots.sh b/desktop/flatpak/scripts/capture-screenshots.sh new file mode 100644 index 00000000..ed03ea8e --- /dev/null +++ b/desktop/flatpak/scripts/capture-screenshots.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Capture review-ui screenshots for Flathub metainfo (#211). +# +# Prerequisites: +# - built/installed flatpak: ./scripts/build-flatpak.sh --install +# - a sample KB with pending proposals (or empty queue is fine for smoke) +# - imagemagick `import` or gnome-screenshot / spectacle available +# +# Output: desktop/flatpak/screenshots/{queue,detail}.png + +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +OUT="${ROOT}/screenshots" +mkdir -p "${OUT}" + +PORT="${VOUCH_SCREENSHOT_PORT:-7780}" +URL="http://127.0.0.1:${PORT}/" + +echo "Starting vouch review-ui on ${URL} (flatpak)…" +flatpak run --command=vouch com.vouchdev.vouch review-ui \ + --bind "127.0.0.1:${PORT}" \ + --no-open-browser & +PID=$! +trap 'kill ${PID} 2>/dev/null || true' EXIT + +for _ in $(seq 1 30); do + if curl -fsS "${URL}" >/dev/null 2>&1; then + break + fi + sleep 0.5 +done + +if command -v import >/dev/null; then + import -window root "${OUT}/queue.png" +else + echo "install imagemagick or capture ${URL} manually into ${OUT}/" >&2 + exit 1 +fi + +echo "wrote ${OUT}/queue.png" +echo "open a proposal detail view and re-run with VOUCH_SCREENSHOT=detail for the second shot" diff --git a/desktop/flatpak/scripts/generate-icons.py b/desktop/flatpak/scripts/generate-icons.py new file mode 100644 index 00000000..411a7453 --- /dev/null +++ b/desktop/flatpak/scripts/generate-icons.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +"""Generate hicolor PNG icons from the scalable SVG (#211). + +Uses only the Python standard library (zlib + struct). Run from repo root: + + python desktop/flatpak/scripts/generate-icons.py +""" + +from __future__ import annotations + +import struct +import zlib +from pathlib import Path + +FLATPAK_DIR = Path(__file__).resolve().parents[1] +SVG_PATH = FLATPAK_DIR / "share/icons/hicolor/scalable/apps/com.vouchdev.vouch.svg" +APP_ID = "com.vouchdev.vouch" +SIZES = (16, 32, 48, 64, 128, 256, 512) + +# Brand palette from docs/banner.svg +COLOR_BG_TOP = (0xF1, 0xF3, 0xF7) +COLOR_BG_BOTTOM = (0xE2, 0xE6, 0xF3) +COLOR_BAR_TOP = (0x4F, 0x5D, 0x9E) +COLOR_BAR_BOTTOM = (0x1E, 0x2A, 0x44) +COLOR_TEXT = (0x1E, 0x2A, 0x44) +COLOR_ACCENT = (0x4F, 0x5D, 0x9E) + + +def _lerp(a: int, b: int, t: float) -> int: + return int(a + (b - a) * t) + + +def _lerp_rgb( + c1: tuple[int, int, int], c2: tuple[int, int, int], t: float +) -> tuple[int, int, int]: + return (_lerp(c1[0], c2[0], t), _lerp(c1[1], c2[1], t), _lerp(c1[2], c2[2], t)) + + +def _render_icon(size: int) -> list[tuple[int, int, int]]: + """Rasterize a simplified vector motif matching the SVG.""" + pixels: list[tuple[int, int, int]] = [] + radius = size * 96 / 512 + bar_x = size * 108 / 512 + bar_w = size * 56 / 512 + bar_y = size * 96 / 512 + bar_h = size * 320 / 512 + circle_cx = size * 392 / 512 + circle_cy = size * 360 / 512 + circle_r = size * 28 / 512 + + for y in range(size): + for x in range(size): + t = (x + y) / (2 * (size - 1)) if size > 1 else 0.0 + bg = _lerp_rgb(COLOR_BG_TOP, COLOR_BG_BOTTOM, t) + + # rounded rect background clip (approx) + dx = min(x, size - 1 - x) + dy = min(y, size - 1 - y) + corner = radius + outside = (dx < corner and dy < corner) and ( + (corner - dx) ** 2 + (corner - dy) ** 2 > corner**2 + ) + if outside: + pixels.append((0, 0, 0)) + continue + + color = bg + + # vertical bar with gradient + if bar_x <= x <= bar_x + bar_w and bar_y <= y <= bar_y + bar_h: + bar_t = (y - bar_y) / bar_h if bar_h else 0 + color = _lerp_rgb(COLOR_BAR_TOP, COLOR_BAR_BOTTOM, bar_t) + + # 'v' stem (simplified block letter) + text_x0 = size * 200 / 512 + text_x1 = size * 310 / 512 + text_y0 = size * 170 / 512 + text_y1 = size * 310 / 512 + if text_x0 <= x <= text_x1 and text_y0 <= y <= text_y1: + color = COLOR_TEXT + + # review circle accent + dist = ((x - circle_cx) ** 2 + (y - circle_cy) ** 2) ** 0.5 + if circle_r - size * 6 / 512 <= dist <= circle_r: + color = COLOR_ACCENT + + pixels.append(color) + return pixels + + +def _write_png(path: Path, size: int, pixels: list[tuple[int, int, int]]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + + def chunk(tag: bytes, data: bytes) -> bytes: + return ( + struct.pack(">I", len(data)) + + tag + + data + + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF) + ) + + raw = bytearray() + for y in range(size): + raw.append(0) # filter type None + start = y * size + for x in range(size): + r, g, b = pixels[start + x] + raw.extend((r, g, b)) + + ihdr = struct.pack(">IIBBBBB", size, size, 8, 2, 0, 0, 0) + png = b"\x89PNG\r\n\x1a\n" + png += chunk(b"IHDR", ihdr) + png += chunk(b"IDAT", zlib.compress(bytes(raw), 9)) + png += chunk(b"IEND", b"") + path.write_bytes(png) + + +def main() -> None: + if not SVG_PATH.is_file(): + raise SystemExit(f"scalable icon missing: {SVG_PATH}") + + for size in SIZES: + pixels = _render_icon(size) + out = ( + FLATPAK_DIR + / "share/icons/hicolor" + / f"{size}x{size}" + / "apps" + / f"{APP_ID}.png" + ) + _write_png(out, size, pixels) + print(f"wrote {out} ({out.stat().st_size} bytes)") + + +if __name__ == "__main__": + main() diff --git a/desktop/flatpak/scripts/generate-requirements.py b/desktop/flatpak/scripts/generate-requirements.py new file mode 100644 index 00000000..a318e425 --- /dev/null +++ b/desktop/flatpak/scripts/generate-requirements.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +"""Regenerate requirements-flatpak.txt from pyproject.toml (#211).""" + +from __future__ import annotations + +import re +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[3] +PYPROJECT = REPO_ROOT / "pyproject.toml" +OUT = Path(__file__).resolve().parents[1] / "requirements-flatpak.txt" + +HEADER = """\ +# Pinned runtime dependencies for the Flatpak [web] install path (#211). +# Mirrors pyproject.toml core + web extras; regenerate with: +# desktop/flatpak/scripts/generate-requirements.py + +""" + + +def main() -> None: + text = PYPROJECT.read_text(encoding="utf-8") + # project.dependencies is under [project] not optional + proj = re.search(r"dependencies = \[(.*?)\]", text, re.DOTALL) + deps: list[str] = [] + if proj: + deps.extend(re.findall(r'"([^"]+)"', proj.group(1))) + web_block = re.search( + r"web = \[(.*?)\]", + text, + re.DOTALL, + ) + if web_block: + deps.extend(re.findall(r'"([^"]+)"', web_block.group(1))) + + # de-dupe preserving order + seen: set[str] = set() + unique: list[str] = [] + for d in deps: + if d not in seen: + seen.add(d) + unique.append(d) + + OUT.write_text(HEADER + "\n".join(unique) + "\n", encoding="utf-8") + print(f"wrote {OUT} ({len(unique)} deps)") + + +if __name__ == "__main__": + main() diff --git a/desktop/flatpak/scripts/install-runtime.sh b/desktop/flatpak/scripts/install-runtime.sh new file mode 100644 index 00000000..146eceec --- /dev/null +++ b/desktop/flatpak/scripts/install-runtime.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# Install org.freedesktop.Platform//23.08 + SDK for local flatpak builds (#211). +set -euo pipefail + +RUNTIME_VERSION="${FLATPAK_RUNTIME_VERSION:-23.08}" + +flatpak install -y --user flathub \ + "org.freedesktop.Platform//${RUNTIME_VERSION}" \ + "org.freedesktop.Sdk//${RUNTIME_VERSION}" \ + "org.freedesktop.Sdk.Extension.python3//${RUNTIME_VERSION}" \ + "org.freedesktop.Platform.Extension.python3//${RUNTIME_VERSION}" + +echo "runtime ${RUNTIME_VERSION} ready" diff --git a/desktop/flatpak/scripts/validate-manifest.py b/desktop/flatpak/scripts/validate-manifest.py new file mode 100644 index 00000000..6508c1e8 --- /dev/null +++ b/desktop/flatpak/scripts/validate-manifest.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +"""Validate flatpak packaging files (#211). + +Exit 0 when no errors; 1 when validation fails. Warnings print but do not fail +unless --strict. +""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +# Allow running as a script without installing the package. +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +from lib.validate import run_all_validations + + +def main() -> int: + parser = argparse.ArgumentParser(description="Validate vouch Flatpak packaging") + parser.add_argument( + "--root", + type=Path, + default=Path(__file__).resolve().parents[1], + help="desktop/flatpak directory", + ) + parser.add_argument( + "--strict", + action="store_true", + help="treat warnings as errors", + ) + args = parser.parse_args() + + report = run_all_validations(args.root) + for issue in report.issues: + prefix = issue.level.upper() + loc = f" ({issue.path})" if issue.path else "" + print(f"{prefix}: {issue.message}{loc}") + + if report.errors: + return 1 + if args.strict and report.warnings: + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/desktop/flatpak/share/icons/hicolor/128x128/apps/com.vouchdev.vouch.png b/desktop/flatpak/share/icons/hicolor/128x128/apps/com.vouchdev.vouch.png new file mode 100644 index 0000000000000000000000000000000000000000..ad7009ac1b19c2f47c5d828df815d4eaffef7b70 GIT binary patch literal 1688 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 X9hp-p__d6100000NkvXXu0mjfO7zwO literal 0 HcmV?d00001 diff --git a/desktop/flatpak/share/icons/hicolor/48x48/apps/com.vouchdev.vouch.png b/desktop/flatpak/share/icons/hicolor/48x48/apps/com.vouchdev.vouch.png new file mode 100644 index 0000000000000000000000000000000000000000..5ac7a39addb4323bcf85619c5095cb41d60e795c GIT binary patch literal 646 zcmV;10(t$3P)`mZ)?gJSCQ541E<}PAK@bE%C=?0>p->QnLZKiCg?>0S%VaUu zHhJAyPah{UcXKYem;Z~r{E=DCnN%wNbbj}!Ypm{v^yVm?#(ci%Cs8W+TS z!gFuFVOlkJKymMz?CVpE91k9?uv5@f3B-8&EpMD)k{~&a4`RQImx^C88IZKb0dXh8 z;`$ei?i)dnl%{GCD1J^Y6pk^vi$y@PngGO{n)7R)F?y!BAO(#J;?3|({u9R_nX<}BTN<~rEx&q{pRV?Ax4uy1SG4e z)Cm+n!=Ef3U^J(=AO%eTVr~>uFZVGTnS7AE#s%^Ao5}e-ObsNcxgro_qnOC-Vrny7 zP~(Hx`%U`AdrYgw0dcpj@n>aBSz~`fc^dmioTd*Hwfl|Dmas(r8^;u-xvC*3W)mHq zd5a}-r3e(#04RPh9+}?6lr?yzJW#*p(g!iO^oLVL?4DQ3D^BBrc;5`ej|*6`&s0d` zfVf{UL+MpatHuG@hy6X3$PAazAV~S^E)h)_G;qyj6+k9vE(l~Fvov5bMiZD!(s;)# gO+QZW`zGrA1BkR=E+HP@8UO$Q07*qoM6N<$f^GyLf&c&j literal 0 HcmV?d00001 diff --git a/desktop/flatpak/share/icons/hicolor/512x512/apps/com.vouchdev.vouch.png b/desktop/flatpak/share/icons/hicolor/512x512/apps/com.vouchdev.vouch.png new file mode 100644 index 0000000000000000000000000000000000000000..581a4d6d5e4354a5581e47a715e4d03a0610acfe GIT binary patch literal 7943 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 Io=)HRFE9T0OaK4? literal 0 HcmV?d00001 diff --git a/desktop/flatpak/share/icons/hicolor/64x64/apps/com.vouchdev.vouch.png b/desktop/flatpak/share/icons/hicolor/64x64/apps/com.vouchdev.vouch.png new file mode 100644 index 0000000000000000000000000000000000000000..aacbe6b2bd9f4bbb2439323d2f3158aafebffe88 GIT binary patch literal 845 zcmV-T1G4;yP)0009NNkl}L1w{x-XJcP5?Aa6Z5}VKSLJ#4s4E_IIDM7#v^|rSO|DDps zJ}N*9`?dkP*gFAy`=tlJ5ry_n1OKzn-TQ^8wJ*|u*qyVu`V&!W?*(x7{leWJh+6w% zxf#Ix*3RGlj;OV_0{CZ6cI6wQ)}90~PRzMmUlFzTtpMJ6pSk%3QET4}VE^}%%&EL|a9Uo;h^ibL=;q0NzXBiM|{n{lxT6A%=Z3 zfc=hnymuZsu=76D-U(p7!XE3%B5Lie0R9{D(e4bQ*4_#*7xM+Y)3a4ddkm2Ke&0F! z@BoeMLjv@&-ze;Pe;Lx--dx(*{|N;68#J^h4cgkb8g#aI8#K0e*P%Xhk}kzJg~;_E X<3>)kj0Xhy00000NkvXXu0mjf=7pxH literal 0 HcmV?d00001 diff --git a/desktop/flatpak/share/icons/hicolor/scalable/apps/com.vouchdev.vouch.svg b/desktop/flatpak/share/icons/hicolor/scalable/apps/com.vouchdev.vouch.svg new file mode 100644 index 00000000..438b63aa --- /dev/null +++ b/desktop/flatpak/share/icons/hicolor/scalable/apps/com.vouchdev.vouch.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + v + + + diff --git a/desktop/flatpak/vouch-review-ui.sh b/desktop/flatpak/vouch-review-ui.sh new file mode 100644 index 00000000..18183202 --- /dev/null +++ b/desktop/flatpak/vouch-review-ui.sh @@ -0,0 +1,16 @@ +#!/bin/sh +# Flatpak entrypoint for the vouch review console (#211). +# +# Runs the local uvicorn server and opens the system browser via the desktop +# portal. KB discovery uses the same rules as the CLI: nearest .vouch/ above +# cwd, or pass --kb / set VOUCH_KB_PATH. + +set -eu + +BIND="${VOUCH_REVIEW_BIND:-127.0.0.1:7780}" + +if [ -n "${VOUCH_KB_PATH:-}" ]; then + exec vouch review-ui --bind "${BIND}" --kb "${VOUCH_KB_PATH}" "$@" +fi + +exec vouch review-ui --bind "${BIND}" "$@" diff --git a/docs/desktop-flatpak.md b/docs/desktop-flatpak.md new file mode 100644 index 00000000..e231c649 --- /dev/null +++ b/docs/desktop-flatpak.md @@ -0,0 +1,43 @@ +# Flatpak desktop install (#211) + +Linux users on Fedora, Arch, and other Flathub-enabled desktops can install the +vouch **review console** as a sandboxed Flatpak instead of pipx. + +## Install (once on Flathub) + +```bash +flatpak install flathub com.vouchdev.vouch +flatpak run com.vouchdev.vouch +``` + +Until the Flathub listing is live, build from source — see +[`desktop/flatpak/README.md`](../desktop/flatpak/README.md). + +## What the package contains + +The Flatpak ships `vouch review-ui`: a local FastAPI server plus browser UI for +approving or rejecting agent proposals. It does **not** replace the full CLI or +MCP server — pair it with `pipx install vouch-kb` or a host adapter for the +propose side of the loop. + +## Filesystem access + +v1 grants `--filesystem=home` so vouch can discover and mutate `.vouch/` trees +under your home directory (project checkouts and optional `~/.vouch/`). Narrower +per-KB permissions may come in a later release. + +## Network + +`--share=network` allows binding `127.0.0.1` for the review UI and loading it +in your browser. The app does not phone home. + +## Packaging source + +All manifests, icons, and validators live under `desktop/flatpak/`. CI runs +`tests/test_flatpak.py` on every PR to keep the manifest aligned with issue +#211 acceptance criteria. + +## Related + +- [Getting started](./getting-started.md) — agent + human review loop +- [GitHub issue #211](https://github.com/vouchdev/vouch/issues/211) — tracking diff --git a/pyproject.toml b/pyproject.toml index b82bb865..1bff8962 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,14 @@ web = [ # channel; without it the WebSocket handshake 500s at runtime. "websockets>=12,<16", ] +desktop = [ + # Alias for review-ui / Flatpak packaging (#211). Same runtime deps as [web]. + "fastapi>=0.115,<1", + "jinja2>=3,<4", + "python-multipart>=0.0.9", + "uvicorn>=0.30,<1", + "websockets>=12,<16", +] dev = [ "pytest>=9.0.3,<10", "pytest-cov>=5,<6", diff --git a/tests/test_flatpak.py b/tests/test_flatpak.py new file mode 100644 index 00000000..2e4a9288 --- /dev/null +++ b/tests/test_flatpak.py @@ -0,0 +1,144 @@ +"""Flatpak packaging contract checks (#211).""" + +from __future__ import annotations + +import json +import xml.etree.ElementTree as ET +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parents[1] +FLATPAK_DIR = REPO_ROOT / "desktop" / "flatpak" + +APP_ID = "com.vouchdev.vouch" +MANIFEST = FLATPAK_DIR / "com.vouchdev.vouch.yaml" + + +@pytest.fixture +def manifest_text() -> str: + return MANIFEST.read_text(encoding="utf-8") + + +def test_flatpak_directory_layout() -> None: + expected = [ + MANIFEST, + FLATPAK_DIR / "com.vouchdev.vouch.desktop", + FLATPAK_DIR / "com.vouchdev.vouch.metainfo.xml", + FLATPAK_DIR / "vouch-review-ui.sh", + FLATPAK_DIR / "requirements-flatpak.txt", + FLATPAK_DIR / "share/icons/hicolor/scalable/apps/com.vouchdev.vouch.svg", + FLATPAK_DIR / "flathub/com.vouchdev.vouch.json", + ] + for path in expected: + assert path.is_file(), f"missing {path.relative_to(REPO_ROOT)}" + + +def test_manifest_runtime_and_permissions(manifest_text: str) -> None: + assert f"id: {APP_ID}" in manifest_text + assert "runtime: org.freedesktop.Platform" in manifest_text + assert ( + "runtime-version: '23.08'" in manifest_text + or 'runtime-version: "23.08"' in manifest_text + ) + assert "command: vouch-review-ui" in manifest_text + assert "--share=network" in manifest_text + assert "--filesystem=home" in manifest_text + + +def test_manifest_installs_web_extra(manifest_text: str) -> None: + assert ".[web]" in manifest_text + assert "name: vouch-desktop" in manifest_text + + +def test_desktop_entry_points_at_launcher() -> None: + text = (FLATPAK_DIR / f"{APP_ID}.desktop").read_text(encoding="utf-8") + assert "Exec=vouch-review-ui" in text + assert f"Icon={APP_ID}" in text + + +def test_metainfo_id_and_launchable() -> None: + tree = ET.parse(FLATPAK_DIR / f"{APP_ID}.metainfo.xml") + root = tree.getroot() + assert root.findtext("id") == APP_ID + launch = root.find("launchable") + assert launch is not None + assert launch.get("type") == "desktop-id" + assert launch.text == f"{APP_ID}.desktop" + + +def test_metainfo_has_release() -> None: + tree = ET.parse(FLATPAK_DIR / f"{APP_ID}.metainfo.xml") + releases = tree.getroot().findall("releases/release") + assert releases, "Flathub needs at least one release" + assert releases[0].get("version") + + +def test_icon_png_sizes() -> None: + sizes = (16, 32, 48, 64, 128, 256, 512) + for size in sizes: + png = ( + FLATPAK_DIR + / "share/icons/hicolor" + / f"{size}x{size}" + / "apps" + / f"{APP_ID}.png" + ) + assert png.is_file(), f"missing icon {size}x{size}" + assert png.stat().st_size > 64 + + +def test_launcher_invokes_review_ui() -> None: + text = (FLATPAK_DIR / "vouch-review-ui.sh").read_text(encoding="utf-8") + assert "vouch review-ui" in text + + +def test_flathub_json_matches_app_id() -> None: + data = json.loads((FLATPAK_DIR / "flathub/com.vouchdev.vouch.json").read_text()) + assert data["id"] == APP_ID + assert data["runtime-version"] == "23.08" + finish = set(data["finish-args"]) + assert "--filesystem=home" in finish + assert "--share=network" in finish + + +def test_requirements_cover_web_stack() -> None: + text = (FLATPAK_DIR / "requirements-flatpak.txt").read_text(encoding="utf-8") + for dep in ("fastapi", "uvicorn", "websockets", "mcp", "pydantic"): + assert dep in text + + +def test_validate_module_reports_clean() -> None: + import sys + + sys.path.insert(0, str(FLATPAK_DIR)) + from lib.validate import run_all_validations + + report = run_all_validations(FLATPAK_DIR) + assert report.ok(), [f"{i.level}: {i.message}" for i in report.errors] + + +def test_manifest_excludes_build_artifacts(manifest_text: str) -> None: + assert "desktop/flatpak/build-dir" in manifest_text + assert ".venv" in manifest_text + + +def test_issue_211_acceptance_documented() -> None: + readme = (FLATPAK_DIR / "README.md").read_text(encoding="utf-8") + assert "23.08" in readme + assert "filesystem=home" in readme or "--filesystem=home" in readme + assert "flathub" in readme.lower() + + +def test_generate_icons_script_is_idempotent() -> None: + script = FLATPAK_DIR / "scripts/generate-icons.py" + assert script.is_file() + # script path referenced in Makefile + makefile = (FLATPAK_DIR / "Makefile").read_text(encoding="utf-8") + assert "generate-icons.py" in makefile + + +def test_flathub_submission_checklist_mentions_commit_pin() -> None: + text = (FLATPAK_DIR / "flathub/SUBMISSION.md").read_text(encoding="utf-8") + assert "REPLACE_WITH_TAG_COMMIT" in text + assert "flatpak install flathub" in text diff --git a/tests/test_flatpak_validate_extended.py b/tests/test_flatpak_validate_extended.py new file mode 100644 index 00000000..7a1dfeed --- /dev/null +++ b/tests/test_flatpak_validate_extended.py @@ -0,0 +1,85 @@ +"""Extended flatpak validation tests (#211).""" + +from __future__ import annotations + +import sys +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parents[1] +FLATPAK_DIR = REPO_ROOT / "desktop" / "flatpak" +sys.path.insert(0, str(FLATPAK_DIR)) + +from lib.validate import ( # noqa: E402 + APP_ID, + ICON_SIZES, + REQUIRED_FINISH_ARGS, + ValidationReport, + validate_desktop_entry, + validate_icons, + validate_launcher_script, + validate_manifest_content, + validate_metainfo, + validate_requirements_pin, +) + + +@pytest.fixture +def report() -> ValidationReport: + return ValidationReport() + + +def test_required_finish_args_frozen() -> None: + assert "--share=network" in REQUIRED_FINISH_ARGS + assert "--filesystem=home" in REQUIRED_FINISH_ARGS + + +def test_manifest_content_clean(report: ValidationReport) -> None: + validate_manifest_content(report, FLATPAK_DIR) + assert not report.errors + + +def test_desktop_entry_clean(report: ValidationReport) -> None: + validate_desktop_entry(report, FLATPAK_DIR) + assert not report.errors + + +def test_metainfo_clean(report: ValidationReport) -> None: + validate_metainfo(report, FLATPAK_DIR) + assert not report.errors + + +def test_icons_all_sizes(report: ValidationReport) -> None: + validate_icons(report, FLATPAK_DIR) + assert not report.errors + assert len(ICON_SIZES) == 7 + + +def test_launcher_clean(report: ValidationReport) -> None: + validate_launcher_script(report, FLATPAK_DIR) + assert not report.errors + + +def test_requirements_clean(report: ValidationReport) -> None: + validate_requirements_pin(report, FLATPAK_DIR) + assert not report.errors + + +def test_app_id_matches_flathub_json() -> None: + import json + + data = json.loads((FLATPAK_DIR / "flathub/com.vouchdev.vouch.json").read_text()) + assert data["id"] == APP_ID + + +@pytest.mark.parametrize("size", ICON_SIZES) +def test_each_icon_png_magic(size: int) -> None: + path = ( + FLATPAK_DIR + / "share/icons/hicolor" + / f"{size}x{size}" + / "apps" + / f"{APP_ID}.png" + ) + assert path.read_bytes()[:8] == b"\x89PNG\r\n\x1a\n" From 1c24e262bc2783d5c39c37b0ebe63a354befea54 Mon Sep 17 00:00:00 2001 From: Clayton Date: Mon, 22 Jun 2026 20:20:20 -0500 Subject: [PATCH 2/4] fix: update --- .github/workflows/flatpak.yml | 7 +++---- desktop/flatpak/README.md | 4 ++-- desktop/flatpak/com.vouchdev.vouch.yaml | 8 -------- desktop/flatpak/flathub/com.vouchdev.vouch.json | 10 ---------- desktop/flatpak/lib/validate.py | 15 +++++++++++++-- desktop/flatpak/scripts/install-runtime.sh | 6 ++---- 6 files changed, 20 insertions(+), 30 deletions(-) diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index d6beaac1..81af2a5c 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -55,9 +55,7 @@ jobs: run: | flatpak install -y --user flathub \ org.freedesktop.Platform//23.08 \ - org.freedesktop.Sdk//23.08 \ - org.freedesktop.Sdk.Extension.python3//23.08 \ - org.freedesktop.Platform.Extension.python3//23.08 + org.freedesktop.Sdk//23.08 - name: generate icons run: python desktop/flatpak/scripts/generate-icons.py @@ -65,7 +63,8 @@ jobs: - name: build run: | cd desktop/flatpak - flatpak-builder --repo=repo --force-clean build-dir com.vouchdev.vouch.yaml + flatpak-builder --repo=repo --force-clean --install-deps-from=flathub \ + build-dir com.vouchdev.vouch.yaml - name: upload bundle artifact uses: actions/upload-artifact@v7 diff --git a/desktop/flatpak/README.md b/desktop/flatpak/README.md index 845bf1dc..98dd56d8 100644 --- a/desktop/flatpak/README.md +++ b/desktop/flatpak/README.md @@ -6,7 +6,7 @@ Targets Fedora 40+, Arch, and any desktop with Flathub. ## Quick start ```bash -# one-time runtime (org.freedesktop.Platform//23.08) +# one-time runtime (org.freedesktop.Platform//23.08 + Sdk; python3 is in the Sdk) ./scripts/install-runtime.sh # build + install for the current user @@ -68,5 +68,5 @@ Acceptance: ## Runtime - **Base:** `org.freedesktop.Platform//23.08` -- **Python:** SDK/runtime `Extension.python3` (3.11) +- **Python:** bundled in `org.freedesktop.Sdk` (no separate Extension.python3 on Flathub) - **Package:** `pip install '.[web]'` — no bundled CPython upgrade automation diff --git a/desktop/flatpak/com.vouchdev.vouch.yaml b/desktop/flatpak/com.vouchdev.vouch.yaml index 744dd646..b82a3495 100644 --- a/desktop/flatpak/com.vouchdev.vouch.yaml +++ b/desktop/flatpak/com.vouchdev.vouch.yaml @@ -9,13 +9,6 @@ id: com.vouchdev.vouch runtime: org.freedesktop.Platform runtime-version: '23.08' sdk: org.freedesktop.Sdk -sdk-extensions: - - org.freedesktop.Sdk.Extension.python3 - -add-extensions: - org.freedesktop.Platform.python3: - version: '23.08' - directory: lib/python3.11 command: vouch-review-ui @@ -34,7 +27,6 @@ modules: - name: vouch buildsystem: simple build-options: - append-path: /usr/lib/sdk/python3/bin env: PIP_DISABLE_PIP_VERSION_CHECK: '1' PYTHONNOUSERSITE: '1' diff --git a/desktop/flatpak/flathub/com.vouchdev.vouch.json b/desktop/flatpak/flathub/com.vouchdev.vouch.json index f6f6e0cb..a8b4c5a2 100644 --- a/desktop/flatpak/flathub/com.vouchdev.vouch.json +++ b/desktop/flatpak/flathub/com.vouchdev.vouch.json @@ -3,15 +3,6 @@ "runtime": "org.freedesktop.Platform", "runtime-version": "23.08", "sdk": "org.freedesktop.Sdk", - "sdk-extensions": [ - "org.freedesktop.Sdk.Extension.python3" - ], - "add-extensions": { - "org.freedesktop.Platform.python3": { - "version": "23.08", - "directory": "lib/python3.11" - } - }, "command": "vouch-review-ui", "finish-args": [ "--share=network", @@ -25,7 +16,6 @@ "name": "vouch", "buildsystem": "simple", "build-options": { - "append-path": "/usr/lib/sdk/python3/bin", "env": { "PIP_DISABLE_PIP_VERSION_CHECK": "1", "PYTHONNOUSERSITE": "1" diff --git a/desktop/flatpak/lib/validate.py b/desktop/flatpak/lib/validate.py index 8bcaecd8..d7402dcd 100644 --- a/desktop/flatpak/lib/validate.py +++ b/desktop/flatpak/lib/validate.py @@ -146,8 +146,19 @@ def validate_manifest_content(report: ValidationReport, root: Path = FLATPAK_DIR for arg in sorted(missing_recommended): report.add("warning", f"finish-args missing recommended {arg}", str(path)) - if "org.freedesktop.Sdk.Extension.python3" not in text: - report.add("error", "sdk-extensions must include python3 extension", str(path)) + if "org.freedesktop.Sdk.Extension.python3" in text: + report.add( + "error", + "do not use Sdk.Extension.python3 — python3 ships in org.freedesktop.Sdk", + str(path), + ) + + if "/usr/lib/sdk/python3" in text: + report.add( + "error", + "do not append-path /usr/lib/sdk/python3 — use the SDK's built-in pip3", + str(path), + ) if "name: vouch" not in text: report.add("error", "modules must include vouch python module", str(path)) diff --git a/desktop/flatpak/scripts/install-runtime.sh b/desktop/flatpak/scripts/install-runtime.sh index 146eceec..614d53ec 100644 --- a/desktop/flatpak/scripts/install-runtime.sh +++ b/desktop/flatpak/scripts/install-runtime.sh @@ -6,8 +6,6 @@ RUNTIME_VERSION="${FLATPAK_RUNTIME_VERSION:-23.08}" flatpak install -y --user flathub \ "org.freedesktop.Platform//${RUNTIME_VERSION}" \ - "org.freedesktop.Sdk//${RUNTIME_VERSION}" \ - "org.freedesktop.Sdk.Extension.python3//${RUNTIME_VERSION}" \ - "org.freedesktop.Platform.Extension.python3//${RUNTIME_VERSION}" + "org.freedesktop.Sdk//${RUNTIME_VERSION}" -echo "runtime ${RUNTIME_VERSION} ready" +echo "runtime ${RUNTIME_VERSION} ready (python3 ships in the SDK)" From 1fe73ddaea5f212cff91471f2506cd1974af56a5 Mon Sep 17 00:00:00 2001 From: Clayton Date: Mon, 22 Jun 2026 20:25:40 -0500 Subject: [PATCH 3/4] fix: update --- .github/workflows/flatpak.yml | 7 +++---- desktop/flatpak/com.vouchdev.vouch.yaml | 14 -------------- desktop/flatpak/lib/validate.py | 8 ++++++++ tests/test_flatpak.py | 6 +++--- 4 files changed, 14 insertions(+), 21 deletions(-) diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index 81af2a5c..1c712b46 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -49,11 +49,11 @@ jobs: sudo apt-get install -y flatpak flatpak-builder - name: add flathub - run: flatpak remote-add --if-not-exists --user flathub https://flathub.org/repo/flathub.flatpakrepo + run: sudo flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo - name: install runtime 23.08 run: | - flatpak install -y --user flathub \ + sudo flatpak install -y flathub \ org.freedesktop.Platform//23.08 \ org.freedesktop.Sdk//23.08 @@ -63,8 +63,7 @@ jobs: - name: build run: | cd desktop/flatpak - flatpak-builder --repo=repo --force-clean --install-deps-from=flathub \ - build-dir com.vouchdev.vouch.yaml + flatpak-builder --repo=repo --force-clean build-dir com.vouchdev.vouch.yaml - name: upload bundle artifact uses: actions/upload-artifact@v7 diff --git a/desktop/flatpak/com.vouchdev.vouch.yaml b/desktop/flatpak/com.vouchdev.vouch.yaml index b82a3495..3e8f84ea 100644 --- a/desktop/flatpak/com.vouchdev.vouch.yaml +++ b/desktop/flatpak/com.vouchdev.vouch.yaml @@ -36,20 +36,6 @@ modules: sources: - type: dir path: ../.. - exclude: - - .git - - .github - - .venv - - .mypy_cache - - .pytest_cache - - .ruff_cache - - build - - dist - - htmlcov - - desktop/flatpak/build-dir - - desktop/flatpak/.flatpak-builder - - '**/__pycache__' - - '**/*.pyc' # --- launcher, desktop entry, metainfo, icons ------------------------------- - name: vouch-desktop diff --git a/desktop/flatpak/lib/validate.py b/desktop/flatpak/lib/validate.py index d7402dcd..ac7b880f 100644 --- a/desktop/flatpak/lib/validate.py +++ b/desktop/flatpak/lib/validate.py @@ -160,6 +160,14 @@ def validate_manifest_content(report: ValidationReport, root: Path = FLATPAK_DIR str(path), ) + if "exclude:" in text and "type: dir" in text: + report.add( + "warning", + "dir sources do not support exclude on older flatpak-builder; " + "use a git source or staging module instead", + str(path), + ) + if "name: vouch" not in text: report.add("error", "modules must include vouch python module", str(path)) diff --git a/tests/test_flatpak.py b/tests/test_flatpak.py index 2e4a9288..62658fa7 100644 --- a/tests/test_flatpak.py +++ b/tests/test_flatpak.py @@ -118,9 +118,9 @@ def test_validate_module_reports_clean() -> None: assert report.ok(), [f"{i.level}: {i.message}" for i in report.errors] -def test_manifest_excludes_build_artifacts(manifest_text: str) -> None: - assert "desktop/flatpak/build-dir" in manifest_text - assert ".venv" in manifest_text +def test_manifest_sources_repo_root(manifest_text: str) -> None: + assert "type: dir" in manifest_text + assert "path: ../.." in manifest_text def test_issue_211_acceptance_documented() -> None: From f067a31be0986faa79925b2cc13e13189da4ca75 Mon Sep 17 00:00:00 2001 From: Clayton Date: Mon, 22 Jun 2026 20:44:37 -0500 Subject: [PATCH 4/4] fix: update --- desktop/flatpak/com.vouchdev.vouch.yaml | 7 +++++-- desktop/flatpak/flathub/com.vouchdev.vouch.json | 7 +++++-- desktop/flatpak/lib/validate.py | 14 ++++++++++++++ tests/test_flatpak.py | 2 ++ 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/desktop/flatpak/com.vouchdev.vouch.yaml b/desktop/flatpak/com.vouchdev.vouch.yaml index 3e8f84ea..be83b8cc 100644 --- a/desktop/flatpak/com.vouchdev.vouch.yaml +++ b/desktop/flatpak/com.vouchdev.vouch.yaml @@ -30,9 +30,12 @@ modules: env: PIP_DISABLE_PIP_VERSION_CHECK: '1' PYTHONNOUSERSITE: '1' + # pip resolves hatchling + runtime deps from PyPI during the build. + build-args: + - --share=network build-commands: - # install from the copied repo root; [web] pulls fastapi/uvicorn/jinja2. - - pip3 install --prefix=${FLATPAK_DEST} --no-build-isolation '.[web]' + # default build isolation pulls hatchling (pyproject build-system) automatically. + - pip3 install --prefix=${FLATPAK_DEST} '.[web]' sources: - type: dir path: ../.. diff --git a/desktop/flatpak/flathub/com.vouchdev.vouch.json b/desktop/flatpak/flathub/com.vouchdev.vouch.json index a8b4c5a2..853c2b14 100644 --- a/desktop/flatpak/flathub/com.vouchdev.vouch.json +++ b/desktop/flatpak/flathub/com.vouchdev.vouch.json @@ -19,10 +19,13 @@ "env": { "PIP_DISABLE_PIP_VERSION_CHECK": "1", "PYTHONNOUSERSITE": "1" - } + }, + "build-args": [ + "--share=network" + ] }, "build-commands": [ - "pip3 install --prefix=${FLATPAK_DEST} --no-build-isolation '.[web]'" + "pip3 install --prefix=${FLATPAK_DEST} '.[web]'" ], "sources": [ { diff --git a/desktop/flatpak/lib/validate.py b/desktop/flatpak/lib/validate.py index ac7b880f..a60018c7 100644 --- a/desktop/flatpak/lib/validate.py +++ b/desktop/flatpak/lib/validate.py @@ -177,6 +177,20 @@ def validate_manifest_content(report: ValidationReport, root: Path = FLATPAK_DIR if ".[web]" not in text: report.add("error", "pip install must include [web] extra for review-ui", str(path)) + if "name: vouch" in text and "build-args:" in text and "--share=network" not in text: + report.add( + "error", + "vouch module build needs --share=network for pip/PyPI during flatpak build", + str(path), + ) + + if "--no-build-isolation" in text and "hatchling" not in text: + report.add( + "warning", + "--no-build-isolation requires hatchling pre-installed in the build sandbox", + str(path), + ) + def validate_desktop_entry(report: ValidationReport, root: Path = FLATPAK_DIR) -> None: path = root / f"{APP_ID}.desktop" diff --git a/tests/test_flatpak.py b/tests/test_flatpak.py index 62658fa7..c4278633 100644 --- a/tests/test_flatpak.py +++ b/tests/test_flatpak.py @@ -49,6 +49,8 @@ def test_manifest_runtime_and_permissions(manifest_text: str) -> None: def test_manifest_installs_web_extra(manifest_text: str) -> None: assert ".[web]" in manifest_text assert "name: vouch-desktop" in manifest_text + assert "--share=network" in manifest_text + assert "build-args:" in manifest_text def test_desktop_entry_points_at_launcher() -> None: