From 801db2871beef22611cf79193d3e2ccb2f94e33b Mon Sep 17 00:00:00 2001 From: pc-heini Date: Tue, 7 Jul 2026 01:56:06 +0200 Subject: [PATCH] feat: Flatpak packaging for the HeiniRemote agent Bundles the agent as de.pc_heini.HeiniRemoteAgent on the freedesktop 24.08 runtime, mirroring the o3de-flatpak CI approach (bwrap-free build, OSTree repo force-pushed to pages, single-file bundle attached to a Gitea release). Compiles the agent from upstream source (newest v* tag, else main). Sandbox notes: screen capture/input via portals need no special permission; the PTY console uses flatpak-spawn --host for a real host shell, and a native install's agent.toml is adopted read-only. Co-Authored-By: Claude Fable 5 --- .gitea/workflows/build-flatpak.yml | 192 ++++++++++++++++++++++ .gitignore | 6 + README.md | 95 +++++++++++ de.pc_heini.HeiniRemoteAgent.desktop | 9 + de.pc_heini.HeiniRemoteAgent.metainfo.xml | 24 +++ de.pc_heini.HeiniRemoteAgent.svg | 9 + heiniremote-wrapper.sh | 24 +++ heiniremote.flatpakrepo | 9 + host-shell.sh | 17 ++ scripts/get-latest-version.sh | 27 +++ scripts/make-flatpak.sh | 74 +++++++++ 11 files changed, 486 insertions(+) create mode 100644 .gitea/workflows/build-flatpak.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 de.pc_heini.HeiniRemoteAgent.desktop create mode 100644 de.pc_heini.HeiniRemoteAgent.metainfo.xml create mode 100644 de.pc_heini.HeiniRemoteAgent.svg create mode 100755 heiniremote-wrapper.sh create mode 100644 heiniremote.flatpakrepo create mode 100755 host-shell.sh create mode 100755 scripts/get-latest-version.sh create mode 100755 scripts/make-flatpak.sh diff --git a/.gitea/workflows/build-flatpak.yml b/.gitea/workflows/build-flatpak.yml new file mode 100644 index 0000000..ae74418 --- /dev/null +++ b/.gitea/workflows/build-flatpak.yml @@ -0,0 +1,192 @@ +name: Build and Publish HeiniRemote Agent Flatpak + +on: + # New upstream release: the daily check rebuilds when HeiniRemote has a + # version (tag or main commit) with no matching tag here yet. + schedule: + - cron: '0 3 * * *' # daily at 03:00 + # Code change: rebuild whenever the packaging itself changes. Scoped to + # `main` on purpose — the job force-pushes the `pages` branch and creates + # tags, and those must NOT re-trigger it (that would loop). + push: + branches: + - main + paths-ignore: + - 'README.md' + - 'docs/**' + - 'LICENSE*' + - '.gitignore' + workflow_dispatch: + inputs: + force: + description: 'Rebuild even if this version was already published' + type: boolean + default: false + +permissions: + contents: write + +jobs: + build: + runs-on: ubuntu-latest + container: + # ubuntu:24.04 matches the freedesktop 24.08 runtime closely enough + # (glibc 2.39, GStreamer 1.24) that the cargo-built binary runs + # unmodified inside the runtime. Privileged so Flatpak's bwrap works + # where needed (runtime install triggers). + image: ubuntu:24.04 + options: --privileged + steps: + - name: Install build dependencies + run: | + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + ca-certificates curl git jq flatpak wl-clipboard \ + build-essential pkg-config \ + libglib2.0-dev libdbus-1-dev libxkbcommon-dev \ + libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \ + libgstreamer-plugins-bad1.0-dev \ + libpipewire-0.3-dev + + # Plain git clone instead of actions/checkout@v4: the bare ubuntu image + # has no Node.js, so JavaScript actions fail with exit 127. + - name: Checkout + env: + TOKEN: ${{ secrets.PUBLISH_TOKEN != '' && secrets.PUBLISH_TOKEN || secrets.GITHUB_TOKEN }} + run: | + AUTH_URL="$(echo "${GITHUB_SERVER_URL}" | sed "s#://#://${GITHUB_ACTOR}:${TOKEN}@#")/${GITHUB_REPOSITORY}.git" + git init -q + git config --global --add safe.directory "$GITHUB_WORKSPACE" + git remote add origin "$AUTH_URL" + git fetch -q --depth 1 origin "${GITHUB_SHA:-$GITHUB_REF_NAME}" + git checkout -q FETCH_HEAD + + - name: Resolve upstream HeiniRemote version + id: ver + env: + TOKEN: ${{ secrets.PUBLISH_TOKEN != '' && secrets.PUBLISH_TOKEN || secrets.GITHUB_TOKEN }} + run: | + chmod +x scripts/*.sh + export UPSTREAM_URL="$(echo "${GITHUB_SERVER_URL}" | sed "s#://#://${GITHUB_ACTOR}:${TOKEN}@#")/pc-heini/HeiniRemote.git" + eval "$(scripts/get-latest-version.sh)" + { + echo "version=$version" + echo "ref=$ref" + } >> "$GITHUB_OUTPUT" + echo "Packaging HeiniRemote $version (ref $ref)" + + - name: Decide whether to build + id: check + run: | + # A code push (or a manual force) rebuilds even if this version is + # already published — the point is to test the changed packaging. + if [ "${{ inputs.force }}" = "true" ] || [ "${{ github.event_name }}" = "push" ]; then + echo "build=true" >> "$GITHUB_OUTPUT" + echo "Rebuild requested (${{ github.event_name }})." + elif git ls-remote --tags origin "refs/tags/v${{ steps.ver.outputs.version }}" | grep -q .; then + echo "build=false" >> "$GITHUB_OUTPUT" + echo "v${{ steps.ver.outputs.version }} already published — nothing to do." + else + echo "build=true" >> "$GITHUB_OUTPUT" + echo "New version v${{ steps.ver.outputs.version }} — building." + fi + + - name: Install Rust + if: steps.check.outputs.build == 'true' + run: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + + - name: Build agent from upstream source + if: steps.check.outputs.build == 'true' + env: + TOKEN: ${{ secrets.PUBLISH_TOKEN != '' && secrets.PUBLISH_TOKEN || secrets.GITHUB_TOKEN }} + run: | + UPSTREAM_URL="$(echo "${GITHUB_SERVER_URL}" | sed "s#://#://${GITHUB_ACTOR}:${TOKEN}@#")/pc-heini/HeiniRemote.git" + # init+fetch instead of clone: works for tags and bare commit shas alike. + rm -rf upstream && git init -q upstream + git -C upstream remote add origin "$UPSTREAM_URL" + git -C upstream fetch -q --depth 1 origin "${{ steps.ver.outputs.ref }}" + git -C upstream checkout -q FETCH_HEAD + cargo build --release -p heiniremote-agent --manifest-path upstream/Cargo.toml + + - name: Install Flatpak runtime and SDK + if: steps.check.outputs.build == 'true' + run: | + flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + flatpak install -y flathub org.freedesktop.Platform//24.08 org.freedesktop.Sdk//24.08 + + - name: Stamp version into AppStream metadata + if: steps.check.outputs.build == 'true' + run: | + sed -i -E \ + "s###" \ + de.pc_heini.HeiniRemoteAgent.metainfo.xml + + - name: Build Flatpak into OSTree repo + if: steps.check.outputs.build == 'true' + run: | + AGENT_BIN=upstream/target/release/heiniremote-agent scripts/make-flatpak.sh + # Single-file bundle for direct installs without adding the remote. + mkdir -p dist + flatpak build-bundle repo \ + "dist/heiniremote-agent-${{ steps.ver.outputs.version }}.flatpak" \ + de.pc_heini.HeiniRemoteAgent stable + # Free disk before publishing (repo/ + dist/ are all we still need). + rm -rf build-dir upstream + + - name: Generate .flatpakrepo + if: steps.check.outputs.build == 'true' + run: | + BASE="${{ github.server_url }}/${{ github.repository }}/raw/branch/pages" + cat > repo/heiniremote.flatpakrepo </dev/null || echo "Asset already attached." diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2cf7565 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +build-dir/ +repo/ +publish/ +upstream/ +dist/ +*.flatpak diff --git a/README.md b/README.md new file mode 100644 index 0000000..1266e88 --- /dev/null +++ b/README.md @@ -0,0 +1,95 @@ +# heiniremote-flatpak + +Flatpak packaging for the [HeiniRemote](https://git.pc-heini.de/pc-heini/HeiniRemote) +customer agent (`de.pc_heini.HeiniRemoteAgent`) — an additional distribution +channel next to the plain release binary, with all dependencies (GStreamer, +libnice, wl-clipboard, …) pinned inside the freedesktop 24.08 runtime. + +Same setup as [o3de-flatpak](https://git.pc-heini.de/pc-heini/o3de-flatpak): +a Gitea Action builds the Flatpak **without** `flatpak-builder` (no bubblewrap +needed in CI), exports it into an OSTree repo and force-pushes that to the +`pages` branch. The difference: the agent is compiled from upstream source +(newest `v*` tag, falling back to `main` while no release exists) instead of +repackaging a prebuilt archive. + +## Install + +From the Flatpak remote (gets updates via `flatpak update`): + +```sh +flatpak remote-add --if-not-exists heiniremote \ + https://git.pc-heini.de/pc-heini/heiniremote-flatpak/raw/branch/pages/heiniremote.flatpakrepo +flatpak install heiniremote de.pc_heini.HeiniRemoteAgent +``` + +Or as a single-file bundle (attached to each release of this repo, no remote +needed, but no auto-updates either): + +```sh +flatpak install ./heiniremote-agent-.flatpak +``` + +## Configure + +The agent needs a server URL and device token. Inside the sandbox the config +path is: + +``` +~/.var/app/de.pc_heini.HeiniRemoteAgent/config/heiniremote/agent.toml +``` + +If a native install already left a config at `~/.config/heiniremote/agent.toml`, +the wrapper picks it up automatically (read-only). `HEINIREMOTE_SERVER_URL` / +`HEINIREMOTE_TOKEN` env overrides work as usual: + +```sh +flatpak run --env=HEINIREMOTE_SERVER_URL=wss://… de.pc_heini.HeiniRemoteAgent +``` + +## Autostart + +The native install runs as a systemd user service; the Flatpak equivalent is a +user unit wrapping `flatpak run`: + +```ini +# ~/.config/systemd/user/heiniremote-agent.service +[Unit] +Description=HeiniRemote agent (Flatpak) +After=graphical-session.target + +[Service] +ExecStart=flatpak run de.pc_heini.HeiniRemoteAgent +Restart=on-failure + +[Install] +WantedBy=graphical-session.target +``` + +## How the sandbox is punched + +Screen capture and input injection go through xdg-desktop-portal +(ScreenCast + RemoteDesktop) — that is the normal, fully sandboxed path and +needs no special permission. Two things do: + +- **`--talk-name=org.freedesktop.Flatpak`** — the PTY rescue console must show + the *host*, not the sandbox. The wrapper sets `SHELL=/app/bin/host-shell`, + which execs the user's real login shell via `flatpak-spawn --host`. This is + a deliberate sandbox escape; for a remote maintenance tool the sandbox is + packaging, not a security boundary. +- **`--filesystem=xdg-config/heiniremote:ro`** — adopt a native install's + `agent.toml` (see above). + +Everything else is standard: `network` (signaling + WebRTC/TURN), `wayland` +(bundled wl-copy/wl-paste), `dri` (dmabuf screencast buffers), and the +StatusNotifier names for the tray icon. + +## Repo layout + +| File | Role | +|---|---| +| `.gitea/workflows/build-flatpak.yml` | CI: resolve upstream version → cargo build → assemble → publish `pages` + release bundle. Daily schedule picks up new upstream versions. | +| `scripts/get-latest-version.sh` | Newest upstream `v*` tag, else `main` HEAD (`0.0.0-git..`). | +| `scripts/make-flatpak.sh` | bwrap-free assembly: `build-init` → copy files → `build-finish` → `build-export`. | +| `heiniremote-wrapper.sh` | Entry point: host-shell `$SHELL` override + config adoption. | +| `host-shell.sh` | `flatpak-spawn --host` bridge for the PTY console. | +| `de.pc_heini.HeiniRemoteAgent.*` | Desktop file, AppStream metainfo (version stamped by CI), icon. | diff --git a/de.pc_heini.HeiniRemoteAgent.desktop b/de.pc_heini.HeiniRemoteAgent.desktop new file mode 100644 index 0000000..cc358d5 --- /dev/null +++ b/de.pc_heini.HeiniRemoteAgent.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Type=Application +Name=HeiniRemote Agent +Comment=Remote maintenance agent — connects this machine to a HeiniRemote server +Exec=heiniremote-wrapper.sh +Icon=de.pc_heini.HeiniRemoteAgent +Terminal=false +Categories=Network;RemoteAccess; +Keywords=remote;support;fernwartung; diff --git a/de.pc_heini.HeiniRemoteAgent.metainfo.xml b/de.pc_heini.HeiniRemoteAgent.metainfo.xml new file mode 100644 index 0000000..61ac999 --- /dev/null +++ b/de.pc_heini.HeiniRemoteAgent.metainfo.xml @@ -0,0 +1,24 @@ + + + de.pc_heini.HeiniRemoteAgent + CC0-1.0 + proprietary + HeiniRemote Agent + Remote maintenance agent for HeiniRemote + +

+ Customer-side agent for HeiniRemote: Wayland screen capture and input + injection via xdg-desktop-portal, plus a PTY rescue console, streamed + over WebRTC to the HeiniRemote management console. Requires a HeiniRemote + server URL and device token (agent.toml). +

+
+ de.pc_heini.HeiniRemoteAgent.desktop + https://pc-heini.de/ + PC-Heini + + + + + +
diff --git a/de.pc_heini.HeiniRemoteAgent.svg b/de.pc_heini.HeiniRemoteAgent.svg new file mode 100644 index 0000000..0899b97 --- /dev/null +++ b/de.pc_heini.HeiniRemoteAgent.svg @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/heiniremote-wrapper.sh b/heiniremote-wrapper.sh new file mode 100755 index 0000000..7f2b56b --- /dev/null +++ b/heiniremote-wrapper.sh @@ -0,0 +1,24 @@ +#!/bin/sh +# Flatpak entry point for the HeiniRemote agent. +# +# Two sandbox adaptations, no agent code changes: +# +# 1. The PTY console (agent's console.rs) spawns $SHELL. Inside the sandbox +# that would be a sandboxed shell showing the Flatpak filesystem — useless +# as a rescue console. Point it at the host-shell bridge instead, which +# escapes to the real host login shell via flatpak-spawn. +# +# 2. Config migration: the agent reads $XDG_CONFIG_HOME/heiniremote/agent.toml, +# which Flatpak redirects to ~/.var/app//config/. If no config exists +# there yet but a native install left one at ~/.config/heiniremote/ +# (visible read-only via --filesystem=xdg-config/heiniremote:ro), use that +# via the agent's HEINIREMOTE_CONFIG override. +export SHELL=/app/bin/host-shell + +if [ -z "${HEINIREMOTE_CONFIG:-}" ] \ + && [ ! -f "${XDG_CONFIG_HOME:-$HOME/.config}/heiniremote/agent.toml" ] \ + && [ -f "$HOME/.config/heiniremote/agent.toml" ]; then + export HEINIREMOTE_CONFIG="$HOME/.config/heiniremote/agent.toml" +fi + +exec /app/bin/heiniremote-agent "$@" diff --git a/heiniremote.flatpakrepo b/heiniremote.flatpakrepo new file mode 100644 index 0000000..3f7db8d --- /dev/null +++ b/heiniremote.flatpakrepo @@ -0,0 +1,9 @@ +[Flatpak Repo] +Title=HeiniRemote Agent +# The CI job regenerates this file on the 'pages' branch with the correct URL, +# so the canonical copy to use is: +# https://git.pc-heini.de/pc-heini/heiniremote-flatpak/raw/branch/pages/heiniremote.flatpakrepo +Url=https://git.pc-heini.de/pc-heini/heiniremote-flatpak/raw/branch/pages +Homepage=https://pc-heini.de/ +Comment=HeiniRemote remote maintenance agent as a Flatpak +Description=Install the HeiniRemote agent on any Linux distribution via Flatpak. diff --git a/host-shell.sh b/host-shell.sh new file mode 100755 index 0000000..e2c2196 --- /dev/null +++ b/host-shell.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# Host-shell bridge for the agent's PTY console. +# +# The wrapper sets SHELL=/app/bin/host-shell, so the agent's PTY console +# spawns this instead of a sandboxed shell. flatpak-spawn --host (requires +# --talk-name=org.freedesktop.Flatpak) runs the command on the host with the +# PTY fds forwarded, giving the remote console a real host login shell. +# +# The inner resolution runs ON THE HOST: flatpak-spawn forwards the sandbox +# environment, so $SHELL would still point at this script — resolve the user's +# real login shell from passwd instead and fix up $SHELL before exec'ing it. +exec flatpak-spawn --host /bin/sh -c ' + shell="$(getent passwd "$(id -un)" | cut -d: -f7)" + [ -x "$shell" ] || shell=/bin/sh + export SHELL="$shell" + exec "$shell" -l +' diff --git a/scripts/get-latest-version.sh b/scripts/get-latest-version.sh new file mode 100755 index 0000000..9af16c6 --- /dev/null +++ b/scripts/get-latest-version.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Resolve which HeiniRemote revision to package. Prints eval-able output: +# version=.> +# ref= +# +# Prefers the newest v* release tag on the upstream repo; while no release tag +# exists yet, falls back to the tip of main (version derived from the commit). +# UPSTREAM_URL may carry credentials (CI injects the actions token for private +# repos); only derived values are printed. +set -euo pipefail + +UPSTREAM_URL="${UPSTREAM_URL:-https://git.pc-heini.de/pc-heini/HeiniRemote.git}" + +tag=$(git ls-remote --tags "$UPSTREAM_URL" 'refs/tags/v*' \ + | awk -F'refs/tags/' '{print $2}' \ + | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \ + | sort -V | tail -n1 || true) + +if [ -n "$tag" ]; then + echo "version=${tag#v}" + echo "ref=$tag" +else + sha=$(git ls-remote "$UPSTREAM_URL" refs/heads/main | cut -c1-12) + [ -n "$sha" ] || { echo "ERROR: cannot resolve upstream main" >&2; exit 1; } + echo "version=0.0.0-git.$(date +%Y%m%d).$sha" + echo "ref=$sha" +fi diff --git a/scripts/make-flatpak.sh b/scripts/make-flatpak.sh new file mode 100755 index 0000000..3d25e54 --- /dev/null +++ b/scripts/make-flatpak.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# Assemble the HeiniRemote agent Flatpak WITHOUT flatpak-builder. +# +# Same approach as o3de-flatpak: flatpak-builder would run build commands +# inside a bubblewrap sandbox (needs user namespaces / privileged CI). We +# don't need that — the real build (cargo) happens outside this script, and +# flatpak build-init/build-finish/build-export only touch files and the +# OSTree repo. +# +# Inputs (env): +# AGENT_BIN path to the compiled heiniremote-agent binary (required) +# WL_CLIPBOARD_DIR dir containing wl-copy/wl-paste to bundle (default /usr/bin) +# +# Produces ./repo (an OSTree repo with branch "stable"). +set -euo pipefail +cd "$(dirname "$0")/.." + +APP_ID=de.pc_heini.HeiniRemoteAgent +RUNTIME_VER=24.08 +BRANCH=stable +AGENT_BIN="${AGENT_BIN:?set AGENT_BIN to the compiled heiniremote-agent}" +WL_DIR="${WL_CLIPBOARD_DIR:-/usr/bin}" + +rm -rf build-dir repo + +echo ">> build-init" +flatpak build-init build-dir "$APP_ID" org.freedesktop.Sdk org.freedesktop.Platform "$RUNTIME_VER" +DEST=build-dir/files + +echo ">> installing agent + bundled tools" +install -Dm755 "$AGENT_BIN" "$DEST/bin/heiniremote-agent" +# wl-clipboard is bundled because the agent's clipboard sync shells out to it +# (clipboard.rs); the freedesktop runtime does not ship it. +install -Dm755 "$WL_DIR/wl-copy" "$DEST/bin/wl-copy" +install -Dm755 "$WL_DIR/wl-paste" "$DEST/bin/wl-paste" +install -Dm755 heiniremote-wrapper.sh "$DEST/bin/heiniremote-wrapper.sh" +install -Dm755 host-shell.sh "$DEST/bin/host-shell" + +echo ">> installing desktop metadata" +install -Dm644 "$APP_ID.desktop" "$DEST/share/applications/$APP_ID.desktop" +install -Dm644 "$APP_ID.metainfo.xml" "$DEST/share/metainfo/$APP_ID.metainfo.xml" +install -Dm644 "$APP_ID.svg" "$DEST/share/icons/hicolor/scalable/apps/$APP_ID.svg" + +echo ">> build-finish (sandbox permissions)" +# ScreenCast/RemoteDesktop run through xdg-desktop-portal, which sandboxed +# apps may always talk to — no extra permission needed. What IS needed: +# network signaling wss + WebRTC/TURN +# wayland wl-copy/wl-paste talk to the compositor +# dri PipeWire screencast buffers may be dmabuf +# xdg-config/heiniremote:ro adopt an existing native install's agent.toml +# StatusNotifier* tray icon (ksni); non-fatal if absent. ksni +# owns org.kde.StatusNotifierItem-{pid}-1 and +# the wrapper exec's the agent, so inside the +# sandbox the pid is deterministically 2 +# org.freedesktop.Flatpak flatpak-spawn --host for the rescue console's +# real host shell — deliberate sandbox escape, +# without it the console would show the sandbox +flatpak build-finish build-dir \ + --command=heiniremote-wrapper.sh \ + --share=network \ + --socket=wayland \ + --device=dri \ + --filesystem=xdg-config/heiniremote:ro \ + --talk-name=org.kde.StatusNotifierWatcher \ + --own-name=org.kde.StatusNotifierItem-2-1 \ + --talk-name=org.freedesktop.Flatpak + +echo ">> export to OSTree repo" +# --disable-sandbox: build-export validates icons in bwrap by default, which +# fails in unprivileged CI containers; this runs the same validation in-process. +flatpak build-export --disable-sandbox repo build-dir "$BRANCH" +flatpak build-update-repo repo --title="HeiniRemote Agent" --prune --prune-depth=1 + +echo ">> done: ./repo"