From ec6c4ff00068aa1e378d37bca186f8cec4d6161c Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Tue, 11 Aug 2026 23:01:49 +0200 Subject: [PATCH 1/2] ci(flatpak): spike how far Flathub actually is MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flathub forbids the route every other channel here takes: "All source available submissions must be built entirely from source code. This requirement applies to the main application component defined in the manifest, as well as any runtime dependencies included in the manifest." OpenScreen is MIT, so repackaging the .deb is out, and extra-data is out too — it exists for non-redistributable sources, and upstream authorship implies the redistribution right. That turns the remaining #335 item from "write a manifest" into a port, and the expensive unknown is ffmpeg. scripts/fetch-ffmpeg.mjs pins a BtbN *prebuilt* shared tree, which Flathub rejects on the same rule, and the tree is not merely convenient: build-linux-compositor-addon.mjs renames every ffmpeg dynamic symbol in the libraries it ships, because Electron links Chromium's own stripped libffmpeg.so into the same process and the addon would otherwise bind to that. It needs headers and libraries it owns, so the runtime's ffmpeg-full extension — libraries, no headers — cannot stand in either. So this is a spike, not a submission. The manifest is built by a dispatch-only workflow whose stages all continue on error, because a spike that stops at the first failure reports one fact and this one reports six: whether the guessed runtime, base and node extension versions exist at all, whether flatpak-node-generator can digest this lockfile, whether both Cargo.locks vendor, and where the build actually dies. The verdict lands in the run summary rather than the log. The ffmpeg sha256 in the manifest is deliberately all zeroes. The pinned build is a BtbN snapshot (n8.1.2-34-g9b6c8969e0), not an upstream release, so there is no honest digest to write yet — stage E resolves what upstream actually publishes and the mismatch is itself a finding. --- .github/workflows/flatpak-spike.yml | 191 ++++++++++++++++++ .../flatpak/com.getopenscreen.OpenScreen.yml | 125 ++++++++++++ 2 files changed, 316 insertions(+) create mode 100644 .github/workflows/flatpak-spike.yml create mode 100644 build/flatpak/com.getopenscreen.OpenScreen.yml diff --git a/.github/workflows/flatpak-spike.yml b/.github/workflows/flatpak-spike.yml new file mode 100644 index 00000000..7d525812 --- /dev/null +++ b/.github/workflows/flatpak-spike.yml @@ -0,0 +1,191 @@ +name: Flatpak spike + +# THROWAWAY. This workflow exists to answer one question — how far from a Flathub +# submission is this repository — and should be deleted once it has answered it. +# It is `workflow_dispatch` only and runs on no event, so it costs nothing until +# someone asks. +# +# Every stage is `continue-on-error` on purpose. A spike that stops at the first +# failure tells you one thing; this one tells you all of them in a single run, and +# writes the verdict to the run summary. Read the summary, not the logs. + +on: + workflow_dispatch: + inputs: + ref: + description: "Ref to spike (defaults to the branch this is dispatched on)" + required: false + type: string + +permissions: + contents: read + +jobs: + spike: + name: How far is Flathub + runs-on: ubuntu-latest + # The full attempt downloads a runtime, an SDK, two SDK extensions, a base app, + # every npm tarball in the lockfile and every crate in two Cargo.locks. If it + # has not concluded in an hour it has concluded something anyway. + timeout-minutes: 60 + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref || github.ref }} + persist-credentials: false + + - name: Install flatpak tooling + run: | + set -euo pipefail + sudo apt-get update -qq + sudo apt-get install -y -qq flatpak flatpak-builder python3-aiohttp python3-toml + flatpak remote-add --if-not-exists --user \ + flathub https://dl.flathub.org/repo/flathub.flatpakrepo + flatpak --version + flatpak-builder --version + + # --------------------------------------------------------------------- + # Stage A is the cheapest and the most likely to invalidate the manifest: + # it asks Flathub what actually exists before anything tries to install it. + # The manifest guesses runtime 24.08, node22 and an Electron2 BaseApp at the + # same version. Any of the three can simply not be published. + - name: "Stage A — what versions actually exist" + id: versions + continue-on-error: true + run: | + set -euo pipefail + echo "## Stage A — availability" >> "$GITHUB_STEP_SUMMARY" + for id in \ + org.freedesktop.Platform \ + org.freedesktop.Sdk \ + org.electronjs.Electron2.BaseApp \ + org.freedesktop.Sdk.Extension.node22 \ + org.freedesktop.Sdk.Extension.node20 \ + org.freedesktop.Sdk.Extension.rust-stable + do + FOUND=$(flatpak remote-ls flathub --user --columns=application,branch 2>/dev/null \ + | awk -v id="$id" '$1 == id { printf "%s ", $2 }' || true) + if [[ -n "$FOUND" ]]; then + echo "- \`$id\` → branches: $FOUND" >> "$GITHUB_STEP_SUMMARY" + else + echo "- \`$id\` → **NOT PUBLISHED**" >> "$GITHUB_STEP_SUMMARY" + fi + done + + - name: "Stage B — install runtime, SDK, base and extensions" + id: install + continue-on-error: true + run: | + set -euo pipefail + flatpak install -y --user --noninteractive flathub \ + org.freedesktop.Platform/x86_64/24.08 \ + org.freedesktop.Sdk/x86_64/24.08 \ + org.electronjs.Electron2.BaseApp/x86_64/24.08 \ + org.freedesktop.Sdk.Extension.node22/x86_64/24.08 \ + org.freedesktop.Sdk.Extension.rust-stable/x86_64/24.08 + + # --------------------------------------------------------------------- + # Stage C is the single biggest unknown for any Electron app on Flathub: + # the sandbox has no network, so every npm tarball must be declared as a + # pinned source. flatpak-node-generator derives that from package-lock.json. + # If it cannot handle this lockfile, nothing downstream matters. + - name: "Stage C — offline npm sources from the lockfile" + id: nodegen + continue-on-error: true + run: | + set -euo pipefail + git clone --depth 1 https://github.com/flathub-infra/flatpak-builder-tools.git /tmp/fbt + python3 /tmp/fbt/node/flatpak-node-generator.py npm package-lock.json \ + -o build/flatpak/generated-sources.json + BYTES=$(stat -c%s build/flatpak/generated-sources.json) + COUNT=$(python3 -c "import json;print(len(json.load(open('build/flatpak/generated-sources.json'))))") + echo "bytes=$BYTES" >> "$GITHUB_OUTPUT" + echo "count=$COUNT" >> "$GITHUB_OUTPUT" + echo "generated $COUNT sources, $BYTES bytes" + + # --------------------------------------------------------------------- + # Stage D: the same problem for Rust. Two lockfiles, and the compositor tree + # pulls bindgen, which wants libclang at build time as well as crates. + - name: "Stage D — vendor both Cargo lockfiles" + id: vendor + continue-on-error: true + run: | + set -euo pipefail + for dir in crates electron/native/pipewire-capture; do + echo "--- $dir ---" + ( cd "$dir" && cargo vendor --versioned-dirs /tmp/vendor-$(basename "$dir") \ + > /tmp/vendor-$(basename "$dir").toml ) + echo "$dir: $(find /tmp/vendor-$(basename "$dir") -maxdepth 1 -type d | wc -l) crates" + done + + # --------------------------------------------------------------------- + # Stage E: the manifest ships a deliberately wrong ffmpeg sha256 so that a + # guessed digest can never reach a submission. Resolve the real one here. + - name: "Stage E — resolve the ffmpeg source digest" + id: ffmpeg + continue-on-error: true + run: | + set -euo pipefail + URL=https://ffmpeg.org/releases/ffmpeg-8.1.2.tar.xz + if curl -fsSL --retry 3 -o /tmp/ffmpeg.tar.xz "$URL"; then + SHA=$(sha256sum /tmp/ffmpeg.tar.xz | awk '{print $1}') + echo "sha=$SHA" >> "$GITHUB_OUTPUT" + echo "$URL → $SHA" + else + echo "::warning::$URL does not exist — the pinned build is BtbN's n8.1.2-34-g9b6c8969e0, which is a *snapshot*, not an upstream release. The manifest needs a real upstream version, and whichever one is chosen is not the tree the addon was tested against." + exit 1 + fi + + # --------------------------------------------------------------------- + # Stage F: the actual build. Expected to fail — the interesting output is + # WHERE. Run it even when earlier stages failed, so the log exists. + - name: "Stage F — flatpak-builder" + id: build + continue-on-error: true + run: | + set -euo pipefail + flatpak-builder --user --install-deps-from=flathub --force-clean \ + --disable-rofiles-fuse \ + /tmp/flatpak-build build/flatpak/com.getopenscreen.OpenScreen.yml \ + 2>&1 | tee /tmp/flatpak-build.log + + - name: Collect artifacts + if: always() + uses: actions/upload-artifact@v7 + with: + name: flatpak-spike + path: | + /tmp/flatpak-build.log + build/flatpak/generated-sources.json + if-no-files-found: warn + retention-days: 7 + + - name: Verdict + if: always() + env: + A: ${{ steps.versions.outcome }} + B: ${{ steps.install.outcome }} + C: ${{ steps.nodegen.outcome }} + D: ${{ steps.vendor.outcome }} + E: ${{ steps.ffmpeg.outcome }} + F: ${{ steps.build.outcome }} + NPM_COUNT: ${{ steps.nodegen.outputs.count }} + FFMPEG_SHA: ${{ steps.ffmpeg.outputs.sha }} + run: | + { + echo "## Verdict" + echo + echo "| Stage | Outcome |" + echo "|---|---|" + echo "| A — versions exist | $A |" + echo "| B — runtime/SDK/base install | $B |" + echo "| C — offline npm sources (${NPM_COUNT:-n/a} entries) | $C |" + echo "| D — cargo vendor, both lockfiles | $D |" + echo "| E — ffmpeg source digest (${FFMPEG_SHA:-unresolved}) | $E |" + echo "| F — flatpak-builder | $F |" + echo + echo "Stage F failing is the expected result, not the finding. The finding is" + echo "which of A–E failed, because those are the ones that decide whether this" + echo "port is a week or a quarter. Read the tail of the build log in the" + echo "\`flatpak-spike\` artifact before drawing any conclusion from F." + } >> "$GITHUB_STEP_SUMMARY" diff --git a/build/flatpak/com.getopenscreen.OpenScreen.yml b/build/flatpak/com.getopenscreen.OpenScreen.yml new file mode 100644 index 00000000..e4d04af3 --- /dev/null +++ b/build/flatpak/com.getopenscreen.OpenScreen.yml @@ -0,0 +1,125 @@ +# Flatpak manifest — SPIKE, not a submission. +# +# The point of this file is to be built by .github/workflows/flatpak-spike.yml and +# FAIL INFORMATIVELY. Flathub forbids the shortcut every other channel in this repo +# takes: "All source available submissions must be built entirely from source code. +# This requirement applies to the main application component defined in the manifest, +# as well as any runtime dependencies included in the manifest." OpenScreen is MIT, +# so it is source-available, so no repackaged .deb and no extra-data escape hatch. +# +# Every value marked SPIKE below is a guess the CI run is supposed to confirm or +# refute. Do not treat this as a reviewed manifest. +app-id: com.getopenscreen.OpenScreen +runtime: org.freedesktop.Platform +# SPIKE: 24.08 is the runtime the Electron2 BaseApp is known to publish against. +# If the spike reports the base is missing for this version, bump both together — +# they must match or flatpak-builder refuses the base outright. +runtime-version: '24.08' +sdk: org.freedesktop.Sdk +base: org.electronjs.Electron2.BaseApp +base-version: '24.08' +sdk-extensions: + # SPIKE: package.json pins node 22.22.1. The freedesktop SDK ships node as a + # versioned extension and node22 may simply not exist for 24.08 — the spike + # enumerates what is actually available before this build is attempted. + - org.freedesktop.Sdk.Extension.node22 + # The two Rust crates (crates/, electron/native/pipewire-capture/) need cargo. + - org.freedesktop.Sdk.Extension.rust-stable +command: openscreen + +finish-args: + - --share=ipc + - --socket=wayland + - --socket=fallback-x11 + - --socket=pulseaudio + # Screen capture goes through the portal, not through a raw socket. The helper in + # electron/native/pipewire-capture already speaks org.freedesktop.portal.ScreenCast, + # which is the sandboxed path, so this part of the port is unusually well placed. + - --talk-name=org.freedesktop.portal.ScreenCast + - --talk-name=org.freedesktop.portal.Desktop + # Webcam. + - --device=all + # Vulkan: the compositor addon is a Vulkan renderer. + - --device=dri + - --share=network + - --filesystem=xdg-videos + - --filesystem=xdg-documents + +build-options: + append-path: /usr/lib/sdk/node22/bin:/usr/lib/sdk/rust-stable/bin + env: + # Keep npm and cargo inside the build dir; the sandbox has no HOME to speak of. + npm_config_cache: /run/build/openscreen/npm-cache + CARGO_HOME: /run/build/openscreen/cargo + +modules: + # --------------------------------------------------------------------------- + # 1. ffmpeg FROM SOURCE. + # + # This is the module that decides whether the port is a week or a quarter, and it + # is the reason the spike exists. scripts/fetch-ffmpeg.mjs pins a BtbN *prebuilt* + # tree (ffmpeg-n8.1.2-…-linux64-lgpl-shared-8.1.tar.xz, sha256 c882a80f…) and + # Flathub will reject it: prebuilt is prebuilt whether or not it is sha-pinned. + # + # Worse, the prebuilt tree is not merely convenient. crates/compositor-view-napi + # RENAMES every ffmpeg dynamic symbol in the libraries it ships, because Electron + # links Chromium's own stripped libffmpeg.so into the same address space and the + # addon would otherwise bind to that. See scripts/build-linux-compositor-addon.mjs + # — it runs `nm -D --defined-only` over the vendored .so files and stages renamed + # copies. That mechanism needs headers AND libraries it owns, which rules out the + # org.freedesktop.Platform.ffmpeg-full extension (libraries, no dev headers). + # + # So: build ffmpeg from the upstream release tarball into /app, with the same LGPL + # shape as the pinned build, and let the existing script rename copies of it. + # SPIKE: the configure flags below are a first cut at matching what the addon and + # the pipewire helper actually link against. Expect this list to be wrong. + - name: ffmpeg + config-opts: + - --disable-static + - --enable-shared + - --disable-programs + - --disable-doc + - --enable-gpl + - --enable-version3 + - --enable-libvpx + - --enable-libopus + sources: + - type: archive + url: https://ffmpeg.org/releases/ffmpeg-8.1.2.tar.xz + # SPIKE: placeholder. The workflow resolves and prints the real digest; do + # not commit a guessed sha256, the build must fail loudly instead. + sha256: 0000000000000000000000000000000000000000000000000000000000000000 + + # --------------------------------------------------------------------------- + # 2. The application. + # + # generated-sources.json is NOT committed: flatpak-node-generator produces it from + # package-lock.json and it is large and entirely derived. The spike workflow writes + # it next to this file before building. If it is absent, that is the first thing to + # check — not a manifest bug. + - name: openscreen + buildsystem: simple + build-options: + env: + # Point the native build at the ffmpeg this manifest just built rather than + # at crates/thirdparty/, which nothing provisions inside the sandbox. + FFMPEG_DIR: /app + build-commands: + # SPIKE: build:linux is `fetch:ffmpeg:sdk && build:native:linux && + # build:native:compositor:linux && tsc && vite build && electron-builder …`. + # The fetch step is a network download and cannot run here, which is why the + # steps are spelled out instead of calling the npm script. + - npm ci --offline + - npm run build:native:linux + - npm run build:native:compositor:linux + - npm run build-vite + # SPIKE: electron-builder's own linux targets are wrong here — Flathub packages + # the app tree directly. `--dir` is the closest thing; whether it cooperates + # with the BaseApp's electron is unknown and is a question for the run. + - npx electron-builder --linux dir --config.npmRebuild=false + - cp -r dist/linux-unpacked /app/openscreen + sources: + - type: git + url: https://github.com/getopenscreen/openscreen.git + tag: v1.9.2 + - generated-sources.json From b56227c26cecd64bd9421230f7bc6d9060e5c16f Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Tue, 11 Aug 2026 23:17:33 +0200 Subject: [PATCH 2/2] fix(flatpak): point the spike at a repository that exists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stage C cloned `flathub-infra/flatpak-builder-tools`, which is a 404, and then invoked `node/flatpak-node-generator.py`, which is not how that tool ships — `node/` is a poetry project exposing a `flatpak-node-generator` console script. Two mistakes stacked on the one stage the spike exists for: whether this lockfile can be turned into offline sources at all. It would have failed for reasons that say nothing about the port. Cloned from `flatpak/`, pinned to 737c008, installed into a venv, so the answer is about the lockfile and stays comparable between runs. Stage A queried the remote once per id behind `|| true`, so a broken remote reported six lines of "NOT PUBLISHED" — the single wrong answer that stage can give, since it reads as "Flathub does not ship the runtime" when it means the question never got asked. One query, status kept, and an explicit QUERY FAILED that draws no conclusion. Both found by CodeRabbit on #347. --- .github/workflows/flatpak-spike.yml | 32 ++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/.github/workflows/flatpak-spike.yml b/.github/workflows/flatpak-spike.yml index 7d525812..26a50976 100644 --- a/.github/workflows/flatpak-spike.yml +++ b/.github/workflows/flatpak-spike.yml @@ -38,7 +38,7 @@ jobs: run: | set -euo pipefail sudo apt-get update -qq - sudo apt-get install -y -qq flatpak flatpak-builder python3-aiohttp python3-toml + sudo apt-get install -y -qq flatpak flatpak-builder python3-venv flatpak remote-add --if-not-exists --user \ flathub https://dl.flathub.org/repo/flathub.flatpakrepo flatpak --version @@ -55,6 +55,22 @@ jobs: run: | set -euo pipefail echo "## Stage A — availability" >> "$GITHUB_STEP_SUMMARY" + # Query ONCE and keep the exit status. Per-id queries behind `|| true` + # turn a broken remote into six "NOT PUBLISHED" lines, which is the one + # wrong answer this stage must never give: it would read as "Flathub does + # not ship the runtime" when it means "the question never got asked". + if ! CATALOG=$(flatpak remote-ls flathub --user --columns=application,branch 2>&1); then + { + echo + echo "**QUERY FAILED** — \`flatpak remote-ls\` errored, so nothing below is a" + echo "statement about what Flathub publishes. Stage A draws no conclusion." + echo + echo '```' + echo "$CATALOG" | tail -5 + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + exit 1 + fi for id in \ org.freedesktop.Platform \ org.freedesktop.Sdk \ @@ -63,8 +79,7 @@ jobs: org.freedesktop.Sdk.Extension.node20 \ org.freedesktop.Sdk.Extension.rust-stable do - FOUND=$(flatpak remote-ls flathub --user --columns=application,branch 2>/dev/null \ - | awk -v id="$id" '$1 == id { printf "%s ", $2 }' || true) + FOUND=$(printf '%s\n' "$CATALOG" | awk -v id="$id" '$1 == id { printf "%s ", $2 }') if [[ -n "$FOUND" ]]; then echo "- \`$id\` → branches: $FOUND" >> "$GITHUB_STEP_SUMMARY" else @@ -94,8 +109,15 @@ jobs: continue-on-error: true run: | set -euo pipefail - git clone --depth 1 https://github.com/flathub-infra/flatpak-builder-tools.git /tmp/fbt - python3 /tmp/fbt/node/flatpak-node-generator.py npm package-lock.json \ + # `node/` is a poetry project exposing the `flatpak-node-generator` console + # script — not a loose .py to invoke by path. Pinned: this tool decides + # whether the whole port is viable, so it should not change under us + # between two runs of the same spike. + git clone --filter=blob:none https://github.com/flatpak/flatpak-builder-tools.git /tmp/fbt + git -C /tmp/fbt checkout 737c0085912f9f7dabf9341d4608e2a77a51a73a + python3 -m venv /tmp/fbt-venv + /tmp/fbt-venv/bin/pip install --quiet /tmp/fbt/node + /tmp/fbt-venv/bin/flatpak-node-generator npm package-lock.json \ -o build/flatpak/generated-sources.json BYTES=$(stat -c%s build/flatpak/generated-sources.json) COUNT=$(python3 -c "import json;print(len(json.load(open('build/flatpak/generated-sources.json'))))")