The PyInstaller-frozen Linux AppImage had no bundled webview, so pywebview fell back to opening the app in the system browser. That was deemed not acceptable. The Flatpak built under the GNOME 49 runtime replaces it with a real native window. The AppImage build was retired in v1.11.0 once the Flatpak shipped — this doc covers the Flatpak's scope, architecture, and the staged build that got it from manifest scaffold to a GitHub-Pages-hosted auto-updating remote.
A frozen (PyInstaller) Linux build can't reuse the host's
PyGObject/WebKit2GTK (they're bound to the system Python), and
bundling that stack into the AppImage is the fragile,
high-maintenance path that pushed the build to browser-fallback in
the first place. The GNOME Flatpak runtime supplies GTK,
WebKit2GTK and PyGObject already, so the app runs under the
runtime's Python with import gi working and pywebview's GTK
backend giving a real native window. Flatpak therefore replaces
PyInstaller for Linux — there is no freeze step in this path.
macOS and Windows are unaffected: they keep their PyInstaller specs and the existing release pipeline.
- Runtime:
org.gnome.Platform// SDKorg.gnome.Sdk(version pinned in the manifest). Brings Python 3, GTK3, WebKit2GTK, PyGObject. - App id:
com.tidaldownloader.Tideway(matches the existingcom.tidaldownloader.appmacOS bundle namespace; Flatpak convention capitalises the final component). - Python deps: generated offline+hashed from
requirements.txtviaflatpak-pip-generatorinto apython3-requirements.jsonmodule (Stage 1). No network during the build sandbox. - PortAudio: the GNOME runtime does not ship libportaudio, and
sounddeviceneeds it. Built as an autotools module in the manifest. - Web bundle: built with the freedesktop Node SDK extension inside
the build sandbox (
npm ci && npm run build), thenweb/distis installed alongside the Python source. - Entry point:
desktop.pyunder the runtime Python. pywebview selects the GTK/WebKit backend (no browser fallback).
--share=network— Tidal / Spotify / Last.fm APIs.--socket=pulseaudio— audio out (PortAudio; PipeWire's pulse shim covers it).--socket=wayland+--socket=fallback-x11+--device=dri— the WebKitGTK window + GPU compositing.--filesystem=xdg-download— downloads land in ~/Downloads.--talk-name=org.freedesktop.Notifications— libnotify desktop notifications.
Kept deliberately narrow; widen only when a feature provably needs it.
- Branch, this doc, manifest scaffold. Done.
flatpak-builderbuilds in the VM. Done. All 20 Python modules + PortAudio + the web bundle build clean under the GNOME runtime sandbox; payload comes out around 526 MB. Two lessons baked into the manifest:- Rust and Cython transitives need prebuilt wheels.
Offline-hashed sdists fail in the sandbox because their
PEP-517 build backends (
maturin,setuptools-rust,Cython) aren't available. We pass an explicit--prefer-wheelslist toflatpak-pip-generatorcovering orjson, curl-cffi, pillow, av, numpy, scipy, rapidfuzz, pydantic-core, watchfiles, httptools, pyyaml, uvloop, aiohttp, zeroconf, cffi. Wheels are arch-conditional (only-arches: x86_64 / aarch64) so the same module file works on either build host. - The npm install needs offline sources too.
npm ciagainst the live registry fails with the sandbox's network-disabled state.flatpak-node-generatorproducesnode-sources.jsonfrompackage-lock.json, the manifest pointsnpm_config_cacheandXDG_CACHE_HOMEat the generatedflatpak-node/tree, andnpm ci --offlinesucceeds.
- Rust and Cython transitives need prebuilt wheels.
Offline-hashed sdists fail in the sandbox because their
PEP-517 build backends (
- Headless VM run. Done. Inside the GNOME 49 sandbox
(Python 3.13.13, PyGObject 3.50.1, GTK 3.0, WebKit2 4.1) the
FastAPI server boots,
Uvicorn running on http://127.0.0.1: 47823lands, and pywebview reaches into the GTK backend trying to create a window. Underxvfb-runthe window creation fails withGtk-WARNING: cannot open displayandInvalid MIT-MAGIC-COOKIE-1 key— the X11 cookie can't be shared into the Flatpak sandbox by xvfb, but the failure mode itself is the proof: pywebview only emits Gtk-WARNINGs because GTK is the backend it selected. Nowebbrowser-fallback log, no missing-giimport, no "GTK cannot be loaded". The native dependency stack (PyAV, numpy, scipy, sounddevice, curl_cffi, etc.) loads at runtime, not just at import. Visual confirmation of an actual window needs a real display and stays out of scope here. - Distribution + auto-updater + CI. Done. Self-hosted over
Flathub: keeps release control on the existing tag-driven
GitHub Actions cadence; no external review treadmill. AppImage
was retired in v1.11.0; the Flatpak is the only Linux artifact.
- 3a — In-app updater.
_running_in_flatpak()(server.py) checks/.flatpak-infoand$FLATPAK_ID. The/api/update-checkresponse carrieskind("flatpak"or"installer"), and/api/update/installreturns HTTP 409 with the exactflatpak update --user com.tidaldownloader.Tidewaycommand inside the sandbox. TheUpdateBannerreadskindand replaces the in-app "Install now" button with that command rendered inline. Pinned bytests/test_update_flatpak.pyandweb/src/components/UpdateBanner.test.tsx. - 3b — CI.
build-linux-flatpakin.github/workflows/ release.ymlinstalls flatpak-builder on a stock ubuntu-22.04 runner, pulls the GNOME 49 runtime + Sdk + Node20 SDK extension from Flathub, runs the manifest with--repo=repo, and produces two artifacts: a.flatpakbundle (attached to the GitHub Release, signed bysign-release.shalongside the DMG / .exe) and the OSTree repo directory. - 3c — Distribution.
publish-flatpak-repodeploys the OSTree repo to thegh-pagesbranch on every tag viapeaceiris/actions-gh-pages. GitHub Pages serves it athttps://j-m-punk.github.io/tideway/, with atideway.flatpakreposubscribe file and anindex.htmllanding page next to the repo. Users runflatpak remote-add --user tideway https://j-m-punk.github.io/tideway/ tideway.flatpakrepoandflatpak install tideway com.tidaldownloader.Tideway;flatpak updatethereafter.
- 3a — In-app updater.
The manifest pins org.gnome.Platform//49 (freedesktop 25.08 base,
Python 3.13). The original scaffold used 47, which went EOL on
2025-10-15. The bump is mechanical: change the runtime-version,
re-pull org.freedesktop.Sdk.Extension.node20//25.08, regenerate
python3-requirements.json with --runtime org.gnome.Sdk//49 so
wheel selection matches the new runtime's Python ABI, rebuild.
The published repo is served over HTTPS from GitHub Pages, so
content authenticity rides on GitHub's TLS chain. The .flatpak
bundle attached to each release is also minisign-signed by the
same scripts/sign-release.sh step that signs the DMG and .exe,
so direct-bundle installs from the release page are end-to-end
verifiable.
Signing OSTree commits with a project GPG key closes the remaining
gap (a compromised gh-pages host could otherwise substitute
malicious commits for users on the auto-updating remote). The CI
side is already wired through release.yml's build-linux-flatpak
job — it's gated on the OSTREE_GPG_KEY_ID secret being present
and is a no-op until you do the one-time setup. See
docs/flatpak-gpg-signing.md for the
key-generation, secret-stashing, and validation steps.