This directory holds the source-of-truth Flatpak manifest,
io.github.surakin.Tesseract.yml.in
(CMake generates the tracked io.github.surakin.Tesseract.yml from it,
substituting the project version into the tesseract module's git tag: —
same pattern as packaging/arch/PKGBUILD.in → PKGBUILD), plus its
supporting files:
io.github.surakin.Tesseract.metainfo.xml— AppStream metadata (description, screenshots, release history).cargo-sources.json— generated offline-vendoring manifest for every Rust dependency, produced from the rootCargo.lockbyflatpak-cargo-generator.py(the Flatpak build has no network access, so every crate download has to be pre-declared as a pinned source).
CI builds the manifest offline on every PR that touches this directory or
the dependency-defining files
(.github/workflows/flatpak.yml),
as a correctness check — it does not sign or publish. Producing a real,
installable release is described below.
Flathub does not accept this app (its 2026-05-29 policy bars AI-assisted
code; see the note at the end). The distribution channel is a self-hosted,
GPG-signed OSTree repo that users add as a Flatpak remote — mechanically
the same thing Flathub is, minus the store listing — published to GitHub
Pages at https://surakin.github.io/tesseract/flatpak-repo/.
This is automatic, in two stages.
- Pushing a
vX.Y.Ztag triggers theflatpakjob in.github/workflows/package.yml, which regeneratescargo-sources.jsonfrom that tag'sCargo.lock, builds and GPG-signs the manifest offline, and attaches a single-file.flatpakbundle to the tag's GitHub pre-release. Nothing reaches the published repo yet. - Marking that release as released (unticking "Set as a pre-release")
triggers the
flatpak-repojob in.github/workflows/publish-release.yml, which imports that same bundle into a GPG-signed OSTree repo (static deltas, pruned) and publishes it to thegh-pagesbranch. Itsdocsjob publishes this repo'sdocs/(the marketing site) to the same branch first.
Neither deploy wipes the other's or a previous release's content — both
use keep_files: true, so the OSTree repo's own update/rollback history
keeps accumulating across releases the way Flatpak expects. See "One-time
setup" below for the prerequisites this depends on (a GPG key, two static
files, and the Pages branch itself).
To re-publish an existing release (e.g. after a failed flatpak-repo run),
run publish-release.yml manually from the Actions tab with its tag and
only the flatpak toggle enabled. For an on-demand publish off an untagged
ref (one-off testing), use
.github/workflows/build-platform.yml
manually with both flatpak and flatpak_publish inputs enabled — it
runs the same sign-and-publish steps off whatever ref you dispatch it
against.
# One-time: the build toolchain
flatpak install flathub org.kde.Platform//6.11 org.kde.Sdk//6.11
# plus flatpak-builder from your distro (e.g. `sudo pacman -S flatpak-builder`)
# Regenerate the tracked .yml for the version you are testing
cmake --preset linux-debug -DTESSERACT_UI=qt6 # any preset; substitutes @PROJECT_VERSION@
# Build into a local OSTree repo, GPG-signed (use your own test key, not the
# release signing key, for local iteration)
flatpak-builder --force-clean --gpg-sign=$GPG_KEYID \
--repo=repo build-dir io.github.surakin.Tesseract.yml
flatpak build-update-repo --generate-static-deltas --prune --prune-depth=20 \
--gpg-sign=$GPG_KEYID reporepo/ is now a self-contained directory of static files you can point a
local Flatpak remote at, without touching the published repo.
GitHub Pages caveats. Published sites have a soft 1 GB size cap
(hence --prune-depth) and a soft 100 GB/month bandwidth cap. A
popular desktop app can blow past the bandwidth limit — front the repo with
Cloudflare (free CDN over Pages) or move it to Cloudflare R2 if that
happens. The OSTree repo is host-agnostic static content, so migrating is a
copy, no manifest change.
flatpak install --user https://surakin.github.io/tesseract/io.github.surakin.Tesseract.flatpakrefThat adds the remote and pulls in the runtime. To add only the remote and
install later: flatpak remote-add --user tesseract https://surakin.github.io/tesseract/tesseract.flatpakrepo, then
flatpak install --user tesseract io.github.surakin.Tesseract.
tesseract.flatpakrepo and io.github.surakin.Tesseract.flatpakref, both
tracked in this directory, are static and committed once — not regenerated
per release, since the signing key and the Pages URL are both stable. Every
publish run (automatic or manual) just copies them alongside repo/ onto
gh-pages.
tesseract.flatpakrepo — adds the remote:
[Flatpak Repo]
Title=Tesseract
Url=https://surakin.github.io/tesseract/flatpak-repo/
Homepage=https://github.com/surakin/tesseract
Comment=Cross-platform Matrix chat client
Description=Tesseract release channel
Icon=https://surakin.github.io/tesseract/flatpak-repo/io.github.surakin.Tesseract.svg
GPGKey=<base64 of `gpg --export $GPG_KEYID`>io.github.surakin.Tesseract.flatpakref — one-click add-remote-and-install:
[Flatpak Ref]
Name=io.github.surakin.Tesseract
Branch=master
Url=https://surakin.github.io/tesseract/flatpak-repo/
Title=Tesseract
Homepage=https://github.com/surakin/tesseract
IsRuntime=false
GPGKey=<base64 of `gpg --export $GPG_KEYID`>
RuntimeRepo=https://flathub.org/repo/flathub.flatpakrepoRuntimeRepo lets a fresh machine pull org.kde.Platform from Flathub
automatically. Once a user has the remote, GNOME Software / KDE Discover
list Tesseract and update it like any other app — which is why the manifest
bothers to install the app-id-named .desktop, icon, and metainfo.
Regenerate both files (with the same Url/Icon, only GPGKey changing)
if the signing key is ever rotated.
Every automatic or manual publish run also builds
tesseract-vX.Y.Z.flatpak and attaches it to the GitHub Release, for users
who don't want to add the remote. flatpak install ./tesseract-vX.Y.Z.flatpak
installs it; unlike the repo, it has no auto-updates — reinstalling the
newer bundle is the only way to update.
flatpak-builder --user --install --force-clean --repo=test-repo \
build-dir io.github.surakin.Tesseract.yml
flatpak run io.github.surakin.TesseractExercise login, sending a message, camera, screen share, notifications, the
tray icon, and /location — the sandboxed capabilities this manifest's
finish-args were scoped around. Then lint:
appstreamcli validate --no-net io.github.surakin.Tesseract.metainfo.xml
desktop-file-validate build-dir/files/share/applications/io.github.surakin.Tesseract.desktopEverything below must land in a commit before the tag is pushed — the tag is what CI builds from, and none of this can be fixed retroactively without re-tagging.
- If
sdk/Cargo.toml'swebrtc-sys/livekitpins moved, re-derive the prebuilt-webrtc archive URL +sha256(the tag iswebrtc-sys-build'sWEBRTC_TAGconstant) and update thearchivesource in.yml.in. - If the matrix-rust-sdk fork bumped its MSRV past
rust-1.97.0, bump therust-toolchainmodule's pinned version +sha256(published athttps://static.rust-lang.org/dist/rust-<ver>-x86_64-unknown-linux-gnu.tar.gz.sha256). - Add a
<release version="X.Y.Z" date="YYYY-MM-DD"/>line (newest first) toio.github.surakin.Tesseract.metainfo.xml, sourced fromCHANGES.md. CI'sflatpakjob fails the whole publish if this is missing. - Cut the release (
git tag vX.Y.Z && git push github vX.Y.Z). CI builds the pre-release — see "Distribution" above. - Once the pre-release has been tested, mark it as released on GitHub to
publish it to the Flatpak repo (and the other channels
publish-release.ymlcovers).
cargo-sources.json no longer needs manual regeneration before tagging; CI
regenerates it from the tagged Cargo.lock itself. The tracked copy in this
directory is only a "last known good" convenience for the local manual
build above.
Required once before the automatic publish flow above can run at all:
-
Generate a dedicated GPG signing key (
gpg --full-generate-key) — don't reuse a key from anything else, so a compromise or rotation here doesn't implicate it. Back up the private key and revocation certificate somewhere outside GitHub. Then add these three repository secrets (Settings → Secrets and variables → Actions):FLATPAK_GPG_PRIVATE_KEY—gpg --export-secret-keys --armor $GPG_KEYID | base64 -w0FLATPAK_GPG_PASSPHRASE— the key's passphrase, if it has oneFLATPAK_GPG_KEY_ID—$GPG_KEYIDitself (not sensitive, kept as a secret alongside the others for simplicity)
CI imports the private key into an ephemeral keyring for the duration of a single job and discards it when the runner is torn down; it is never written to the repo or any commit.
-
Create
tesseract.flatpakrepoandio.github.surakin.Tesseract.flatpakrefin this directory from the templates under "User-facing files" above, withGPGKeyset togpg --export $GPG_KEYID | base64 -w0. Commit them — they're static and reused by every publish run. -
Point GitHub Pages at the
gh-pagesbranch (Settings → Pages → Source), instead ofmain. Ifgh-pagesdoesn't exist yet, create an empty one first so the option is selectable, or just wait for the first successfulpublish-release.ymlrun to create it.
The manifest is still a valid Flathub submission and the per-release
checklist above is what a Flathub update would need too. But Flathub's
2026-05-29 policy prohibits AI-assisted code
across app code, manifests, and metadata, with a discretionary carve-out for
"mature, well-maintained projects". If that carve-out is ever granted, the
Flathub-published copy would live in a separate
flathub/io.github.surakin.Tesseract repo and its buildbot would rebuild on
push; nothing in this directory changes.
Three build-system obstacles had to be worked around to make an offline build succeed (flatpak-builder runs each module's build phase with no network; the CI check and a release build both depend on it):
- Root
CMakeLists.txtFetchContents Corrosion and nlohmann_json from GitHub at CMake configure time. The manifest stages pinned-commit copies as extragitsources under_deps-src/and points CMake at them via-DFETCHCONTENT_SOURCE_DIR_CORROSION=.../-DFETCHCONTENT_SOURCE_DIR_NLOHMANN_JSON=...+-DFETCHCONTENT_FULLY_DISCONNECTED=ON. tests/CMakeLists.txtunconditionallyFetchContents Catch2 — but packaging never runsctestanyway (seepackaging/arch/aur/README.md's note on this), so the manifest just passes-DTESSERACT_BUILD_TESTS=OFF(a CMake option added specifically for this) and skips the problem entirely.webrtc-sys-build'sbuild.rs(pulled in vialivekit, pinnedwebrtc-sys = "=0.3.51"insdk/Cargo.toml) downloads a prebuilt libwebrtc archive over HTTP duringcargo build— not somethingflatpak-cargo-generator.pycan vendor, since it isn't a crates.io dependency. The manifest adds that exact archive as a pinned, checksummedarchivesource and setsLK_CUSTOM_WEBRTCto point at it; a small addition to the rootCMakeLists.txt(corrosion_set_env_vars(... LK_CUSTOM_WEBRTC=...), guarded on the env var being set) forwards it into Cargo's build-script environment, alongside the othercorrosion_set_env_varscalls there.- webrtc-sys compiles its C++ with clang against libwebrtc's hermetic libc++
and its build script panics on a clang older than 21, so the manifest
enables the
org.freedesktop.Sdk.Extension.llvm21SDK extension (sdk-extensions:) and puts/usr/lib/sdk/llvm21/binon thetesseractmodule'sPATH. The extension must be installed next to the runtime (the CI workflows do this) and its branch must match the oneorg.kde.Sdkdeclares fororg.freedesktop.Sdk.Extension.*(25.08).
The matrix-rust-sdk dependency is a git pin (surakin/matrix-rust-sdk, a
fork), not a crates.io release. flatpak-cargo-generator.py handles this: it
emits a git source that clones the fork at the pinned rev, shell steps that
copy each workspace crate into cargo/vendor/, and a [source."…"]
replace-with stanza in the generated cargo/config so the offline build
resolves the git dependency from the vendored copy.
ui/linux-qt/CMakeLists.txt also unconditionally renames the installed
binary/desktop-file/icon from tesseract to tesseract-matrix (to coexist
with the unrelated Tesseract OCR engine, which owns /usr/bin/tesseract on
most distros). The manifest's command: is tesseract-matrix to match, and
a post-install step derives the app-id-named
io.github.surakin.Tesseract.desktop/.svg (which the desktop software
centres need in order to list and update the app) from the renamed artifacts
CMake's own install step already produced.
Camera access uses --device=all — Flatpak has no Camera-specific device
class, and Tesseract talks to /dev/videoN directly via GStreamer's
v4l2src (no portal). This is the same limitation every other raw-V4L2
Flatpak app hits (e.g. Cheese); if the app is ever submitted to Flathub,
expect reviewers to ask for a short justification rather than reject it.