From 8dce4d3e5368274e22cc12ac39b8726a480df4e7 Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Wed, 12 Aug 2026 09:47:31 +0200 Subject: [PATCH 01/12] fix(flatpak): give the spike a digest it can parse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run 31537831529 never attempted the ffmpeg module. The placeholder sha256 was 64 unquoted zeroes, YAML read it as an integer, flatpak-builder could not deserialize it as a string and refused with "No checksum specified" before downloading anything. Stage F reported a failure that was about YAML, not about ffmpeg. Stage E had already resolved the real digest in the same run, and it also settled the question the placeholder existed for: upstream publishes 8.1.2, so there IS an honest source tarball — the pin is BtbN's n8.1.2-34-g9b6c8969e0, 34 commits later. Quoted this time. --- build/flatpak/com.getopenscreen.OpenScreen.yml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/build/flatpak/com.getopenscreen.OpenScreen.yml b/build/flatpak/com.getopenscreen.OpenScreen.yml index e4d04af3..58b6dd61 100644 --- a/build/flatpak/com.getopenscreen.OpenScreen.yml +++ b/build/flatpak/com.getopenscreen.OpenScreen.yml @@ -86,9 +86,19 @@ modules: 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 + # Resolved by stage E of the first spike run (31537831529), not guessed. + # + # Upstream DOES publish 8.1.2, which the first run settled — the pin in + # fetch-ffmpeg.mjs is BtbN's n8.1.2-34-g9b6c8969e0, so this tarball is the + # same release 34 commits earlier. Whether those 34 commits matter to the + # symbol-renaming addon is now the open question, and it is a much smaller + # one than "does an upstream source tarball exist at all". + # + # QUOTED on purpose. Unquoted, the previous all-zeroes placeholder was + # parsed as a YAML integer, flatpak-builder failed to deserialize it as a + # string, and the build died with "No checksum specified" before it + # downloaded anything — so run 1 never actually attempted this module. + sha256: "464beb5e7bf0c311e68b45ae2f04e9cc2af88851abb4082231742a74d97b524c" # --------------------------------------------------------------------------- # 2. The application. From 49ad48c53ed24f38ab11828a3023e554806d01cb Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Wed, 12 Aug 2026 15:36:17 +0200 Subject: [PATCH 02/12] feat(flatpak): wire the offline inputs the build actually asked for MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run 31575509206 got ffmpeg compiled and then died in `npm ci --offline` with ENOTCACHED on zustand. Not an npm problem: the cache paths were invented. The upstream Electron manifest documents them, and they are positional — flatpak-node-generator lays its output out under `flatpak-node/` in the module build dir, so both variables have to name that exact path: XDG_CACHE_HOME: /run/build/openscreen/flatpak-node/cache npm_config_cache: /run/build/openscreen/flatpak-node/npm-cache XDG_CACHE_HOME looked like a nicety and is not: it is where the Electron binary download is cached, so without it electron's postinstall reaches for the network that the sandbox does not have. Cargo gets the same treatment — CARGO_HOME must be the path flatpak-cargo-generator hardcodes, since it writes its replace-with config there and vendors into $CARGO_HOME/vendor. Stage D now emits real source lists for both lockfiles instead of only proving they resolve, and it counts the destinations the two share, because the generator has no way to separate their vendor directories and crates common to both trees land on the same path twice. Better reported than discovered inside flatpak-builder. Electron needs a zypak wrapper or it fails on the SUID sandbox helper, so there is a launcher script and `command:` finally resolves to something. Also here: a desktop file named after the app id, icons renamed on the way in because Flatpak only exports app-id-named ones, and the metainfo launchable rewritten at install time — that file's own comment already said a Flatpak build would have to, and now something does. Rewritten rather than edited, so it stays correct for the deb, and the rewrite fails loudly if it ever stops matching. The git source is a `type: dir` for now. It cannot be the tag Flathub wants: v1.9.2 predates both the metainfo and the desktop file, so a tagged build would fail on files that do not exist in it. Stage E is deleted. It resolved a digest that is now pinned in the manifest, and its only other act was to report "does not exist" for a connection reset — the same conflation of a broken query with a negative answer that the review of #347 corrected in Stage A. --- .github/workflows/flatpak-spike.yml | 75 ++++---- .../com.getopenscreen.OpenScreen.desktop | 15 ++ .../flatpak/com.getopenscreen.OpenScreen.yml | 176 ++++++++++-------- 3 files changed, 156 insertions(+), 110 deletions(-) create mode 100644 build/flatpak/com.getopenscreen.OpenScreen.desktop diff --git a/.github/workflows/flatpak-spike.yml b/.github/workflows/flatpak-spike.yml index 26a50976..7f136ec9 100644 --- a/.github/workflows/flatpak-spike.yml +++ b/.github/workflows/flatpak-spike.yml @@ -126,41 +126,44 @@ jobs: 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" + # Stage D: the same problem for Rust, for both lockfiles. `cargo vendor` was + # enough to answer "do these resolve"; the manifest needs the generator's + # output, which is a source list flatpak-builder can fetch offline. + - name: "Stage D — cargo sources for both 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 + /tmp/fbt-venv/bin/pip install --quiet tomlkit aiohttp + gen() { + /tmp/fbt-venv/bin/python /tmp/fbt/cargo/flatpak-cargo-generator.py "$1" -o "$2" + echo "$2: $(python3 -c "import json,sys;print(len(json.load(open('$2'))))") sources" + } + gen crates/Cargo.lock build/flatpak/cargo-sources-crates.json + gen electron/native/pipewire-capture/Cargo.lock build/flatpak/cargo-sources-pipewire.json + # The generator hardcodes $CARGO_HOME/vendor, so these two lists overlap on + # every crate both trees share. Report the collision count rather than + # discovering it inside flatpak-builder. + python3 - <<'PY' + import json + a = {s.get('dest') for s in json.load(open('build/flatpak/cargo-sources-crates.json'))} + b = {s.get('dest') for s in json.load(open('build/flatpak/cargo-sources-pipewire.json'))} + shared = {d for d in a & b if d} + print(f"shared destinations: {len(shared)}") + for d in sorted(shared)[:10]: + print(f" {d}") + PY - # --------------------------------------------------------------------- - # 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 E is gone. It resolved the ffmpeg digest, which now lives pinned in the + # manifest, so all it could do was re-download 8.1.2 to confirm a constant. + # It also demonstrated the failure mode this workflow was already corrected + # for once: it reported "does not exist" for `curl: (35) Connection reset by + # peer`, turning a network flake into a false claim about upstream. A stage + # that can only be redundant or wrong is worth deleting rather than fixing. # --------------------------------------------------------------------- - # Stage F: the actual build. Expected to fail — the interesting output is - # WHERE. Run it even when earlier stages failed, so the log exists. + # Stage F: the actual build. Run it even when earlier stages failed, so the + # log exists. - name: "Stage F — flatpak-builder" id: build continue-on-error: true @@ -179,6 +182,8 @@ jobs: path: | /tmp/flatpak-build.log build/flatpak/generated-sources.json + build/flatpak/cargo-sources-crates.json + build/flatpak/cargo-sources-pipewire.json if-no-files-found: warn retention-days: 7 @@ -189,10 +194,8 @@ jobs: 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" @@ -202,12 +205,12 @@ jobs: 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 "| D — cargo sources, both lockfiles | $D |" 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." + echo "A–D green means the offline inputs are all wired. F is now the whole" + echo "question: ffmpeg is known to compile, so a failure past it is about the" + echo "app module — the npm cache paths, the shared cargo vendor directory, or" + echo "the Electron packaging. Read the tail of the build log in the" + echo "\`flatpak-spike\` artifact; a green F still does not mean the app RUNS." } >> "$GITHUB_STEP_SUMMARY" diff --git a/build/flatpak/com.getopenscreen.OpenScreen.desktop b/build/flatpak/com.getopenscreen.OpenScreen.desktop new file mode 100644 index 00000000..ab5ad4c6 --- /dev/null +++ b/build/flatpak/com.getopenscreen.OpenScreen.desktop @@ -0,0 +1,15 @@ +[Desktop Entry] +Type=Application +Name=OpenScreen +GenericName=Screen Recorder +Comment=Screen recorder and video editor +Exec=openscreen %U +Icon=com.getopenscreen.OpenScreen +Terminal=false +Categories=AudioVideo;AudioVideoEditing;Recorder; +Keywords=screen;record;recorder;capture;screencast;video;editor; +# Electron derives the WM class from productName, which is "Openscreen" in +# electron-builder.json5 — not the app id. Without this the window does not +# associate with the launcher icon under Wayland or GNOME. +StartupWMClass=Openscreen +StartupNotify=true diff --git a/build/flatpak/com.getopenscreen.OpenScreen.yml b/build/flatpak/com.getopenscreen.OpenScreen.yml index 58b6dd61..17aea373 100644 --- a/build/flatpak/com.getopenscreen.OpenScreen.yml +++ b/build/flatpak/com.getopenscreen.OpenScreen.yml @@ -1,29 +1,27 @@ -# Flatpak manifest — SPIKE, not a submission. +# Flatpak manifest for Flathub. # -# 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. +# Still driven by .github/workflows/flatpak-spike.yml rather than submitted: the +# offline npm cache, the vendored crates and ffmpeg-from-source are all wired now, +# but nobody has launched the resulting app. Do not open a flathub/flathub PR on +# this until someone has. # -# 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. +# Why it is built from source at all, rather than repackaging the .deb like every +# other channel in this repo: "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 that rule applies, and extra-data is not +# an escape hatch — it exists for non-redistributable sources. 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' +# Same as the upstream Electron quick-start manifest: the locale splitting confuses +# an Electron tree that ships its own locales under resources/. +separate-locales: false 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 @@ -32,47 +30,32 @@ finish-args: - --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. + # Screen capture goes through the portal rather than a raw socket, which is the + # one part of this port that was already sandbox-shaped: the helper in + # electron/native/pipewire-capture speaks org.freedesktop.portal.ScreenCast. - --talk-name=org.freedesktop.portal.ScreenCast - --talk-name=org.freedesktop.portal.Desktop # Webcam. - --device=all - # Vulkan: the compositor addon is a Vulkan renderer. + # 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. + # 1. ffmpeg, from source. # - # 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). + # Proven to build in run 31575509206 (~7 minutes). It cannot be the BtbN prebuilt + # tree that scripts/fetch-ffmpeg.mjs pins — Flathub rejects prebuilt dependencies + # — and it cannot be org.freedesktop.Platform.ffmpeg-full either: that extension + # ships libraries without dev headers, and crates/compositor-view-napi needs + # headers AND libraries it owns, because it renames every ffmpeg dynamic symbol so + # the addon cannot bind to Chromium's own stripped libffmpeg.so. # - # 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. + # KNOWN GAP: 8.1.2 upstream is the BtbN pin minus 34 commits. It compiles; whether + # those commits matter to the renaming has not been exercised by running the app. - name: ffmpeg config-opts: - --disable-static @@ -86,50 +69,95 @@ modules: sources: - type: archive url: https://ffmpeg.org/releases/ffmpeg-8.1.2.tar.xz - # Resolved by stage E of the first spike run (31537831529), not guessed. - # - # Upstream DOES publish 8.1.2, which the first run settled — the pin in - # fetch-ffmpeg.mjs is BtbN's n8.1.2-34-g9b6c8969e0, so this tarball is the - # same release 34 commits earlier. Whether those 34 commits matter to the - # symbol-renaming addon is now the open question, and it is a much smaller - # one than "does an upstream source tarball exist at all". - # - # QUOTED on purpose. Unquoted, the previous all-zeroes placeholder was - # parsed as a YAML integer, flatpak-builder failed to deserialize it as a - # string, and the build died with "No checksum specified" before it - # downloaded anything — so run 1 never actually attempted this module. + # Resolved by a spike run, not guessed. QUOTED: unquoted, a 64-digit value + # is a YAML integer, and flatpak-builder then reports "No checksum + # specified" and never downloads the tarball at all. sha256: "464beb5e7bf0c311e68b45ae2f04e9cc2af88851abb4082231742a74d97b524c" # --------------------------------------------------------------------------- # 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: + append-path: /usr/lib/sdk/node22/bin:/usr/lib/sdk/rust-stable/bin env: - # Point the native build at the ffmpeg this manifest just built rather than - # at crates/thirdparty/, which nothing provisions inside the sandbox. + # THE PATHS MATTER, and getting them wrong is why the first two spike runs + # died in `npm ci`. flatpak-node-generator lays its output out relative to + # the module build dir as `flatpak-node/`, so both of these are + # /run/build//flatpak-node/… — the module is named openscreen. + # + # XDG_CACHE_HOME is not optional despite looking like a nicety: it is where + # the Electron binary download is cached, so electron's postinstall goes to + # the network without it and the sandbox has none. + XDG_CACHE_HOME: /run/build/openscreen/flatpak-node/cache + npm_config_cache: /run/build/openscreen/flatpak-node/npm-cache + # Same shape for cargo: flatpak-cargo-generator hardcodes its vendor + # directory as $CARGO_HOME/vendor and writes the replace-with config into + # $CARGO_HOME, so CARGO_HOME has to be this exact path. + CARGO_HOME: /run/build/openscreen/cargo + CARGO_NET_OFFLINE: 'true' + # crates/thirdparty/ffmpeg-linux64-lgpl-shared is what the native build + # scripts look for and nothing provisions it here. Module 1 installed its + # headers and libraries into /app, which is the shape they expect. 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`, not `npm run build:linux`: that script starts with + # `fetch:ffmpeg:sdk`, which is a network download, and module 1 already + # provides what it would have fetched. The remaining steps are spelled out. - 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 + # `dir`, because Flathub packages the app tree and has no use for a .deb or an + # AppImage. executableName is pinned so the wrapper below can name the binary + # instead of discovering it. + - npx electron-builder --linux dir --config.npmRebuild=false --config.executableName=openscreen - cp -r dist/linux-unpacked /app/openscreen + - install -Dm755 openscreen.sh /app/bin/openscreen + - install -Dm644 build/flatpak/com.getopenscreen.OpenScreen.desktop /app/share/applications/${FLATPAK_ID}.desktop + # The metainfo's launchable is `openscreen.desktop`, which is what the deb + # installs and what that file's own comment says a Flatpak build must rewrite. + # Rewriting it here rather than editing the source keeps the file honest for + # the deb, and Flatpak only exports desktop files named after the app id, so + # an unrewritten launchable makes the app unlaunchable from a software centre. + - | + sed "s|>openscreen\.desktop<|>${FLATPAK_ID}.desktop<|" \ + build/com.getopenscreen.OpenScreen.metainfo.xml > metainfo.rewritten.xml + grep -q ">${FLATPAK_ID}.desktop<" metainfo.rewritten.xml \ + || { echo "launchable rewrite matched nothing — did the metainfo change?" >&2; exit 1; } + install -Dm644 metainfo.rewritten.xml /app/share/metainfo/${FLATPAK_ID}.metainfo.xml + # Flatpak only exports icons named after the app ID, so the repo's bare + # `512x512.png` names have to be rewritten on the way in. + - | + for size in 16 24 32 48 64 128 256 512; do + install -Dm644 "icons/icons/png/${size}x${size}.png" \ + "/app/share/icons/hicolor/${size}x${size}/apps/${FLATPAK_ID}.png" + done sources: - - type: git - url: https://github.com/getopenscreen/openscreen.git - tag: v1.9.2 + # SPIKE ONLY, and it must change before any submission: Flathub wants a + # `type: git` pinned to a tag AND its commit sha. It cannot be that yet — + # v1.9.2 predates both build/com.getopenscreen.OpenScreen.metainfo.xml and the + # desktop file next to this manifest, so a tagged build would fail on files + # that do not exist in it. Building the working tree also means the spike tests + # the branch under review rather than a release from before the work started. + # Switch to tag + commit once a release ships containing both files. + - type: dir + path: ../.. + # Generated by the spike workflow, not committed: both are derived from + # lockfiles and large. generated-sources.json alone is ~900 KB. - generated-sources.json + # TWO cargo lockfiles, and the generator has no option to separate their + # vendor directories — both land in $CARGO_HOME/vendor and both write the same + # replace-with config. Crates common to the two (anyhow, bindgen…) therefore + # appear twice at the same destination. If flatpak-builder refuses that, the + # fix is to build each crate tree under its own CARGO_HOME rather than to + # merge the lockfiles. + - cargo-sources-crates.json + - cargo-sources-pipewire.json + # Zypak is mandatory, not a nicety: without it Electron's sandbox fails with + # "SUID sandbox helper binary" and the app does not start. + - type: script + dest-filename: openscreen.sh + commands: + - export TMPDIR="$XDG_RUNTIME_DIR/app/$FLATPAK_ID" + - exec zypak-wrapper /app/openscreen/openscreen "$@" From 8f6f93a983295353a0d25e019ef63117621b75da Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Wed, 12 Aug 2026 16:26:36 +0200 Subject: [PATCH 03/12] feat(flatpak): give bindgen the libclang the SDK does not ship MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run 31602275749 cleared everything the previous one could not: `npm ci --offline` succeeded, so the cache paths were the whole ENOTCACHED story; the two cargo source lists merged into one vendor directory without the destination collision they were expected to cause; the C shim compiled; and FFMPEG_DIR=/app resolved, with avcodec/avformat/avutil/swscale/swresample linked out of the module built one step earlier. It then died in bindgen: "Unable to find libclang". The freedesktop SDK does not carry it, so the LLVM extension is required — llvm18, matching the 24.08 base toolchain — plus LIBCLANG_PATH, which build-linux-compositor-addon.mjs reads rather than searching for. The name is a guess, and the run no longer has to be spent finding out: stage A now enumerates every org.freedesktop.Sdk.Extension published for 24.08, and stage B installs llvm18 explicitly so a wrong name fails in ninety seconds instead of eight minutes. --- .github/workflows/flatpak-spike.yml | 17 ++++++++++++++++- build/flatpak/com.getopenscreen.OpenScreen.yml | 13 ++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/.github/workflows/flatpak-spike.yml b/.github/workflows/flatpak-spike.yml index 7f136ec9..b7a999c2 100644 --- a/.github/workflows/flatpak-spike.yml +++ b/.github/workflows/flatpak-spike.yml @@ -86,6 +86,20 @@ jobs: echo "- \`$id\` → **NOT PUBLISHED**" >> "$GITHUB_STEP_SUMMARY" fi done + # Enumerate rather than guess. Run 31602275749 died in bindgen with + # "Unable to find libclang", so an LLVM extension is required and its exact + # name is not something to discover one 8-minute run at a time. This prints + # every SDK extension published for this runtime version. + { + echo + echo "
All org.freedesktop.Sdk.Extension.* on 24.08" + echo + printf '%s\n' "$CATALOG" \ + | awk '$1 ~ /^org\.freedesktop\.Sdk\.Extension\./ && $2 == "24.08" { print "- `" $1 "`" }' \ + | sort + echo + echo "
" + } >> "$GITHUB_STEP_SUMMARY" - name: "Stage B — install runtime, SDK, base and extensions" id: install @@ -97,7 +111,8 @@ jobs: 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 + org.freedesktop.Sdk.Extension.rust-stable/x86_64/24.08 \ + org.freedesktop.Sdk.Extension.llvm18/x86_64/24.08 # --------------------------------------------------------------------- # Stage C is the single biggest unknown for any Electron app on Flathub: diff --git a/build/flatpak/com.getopenscreen.OpenScreen.yml b/build/flatpak/com.getopenscreen.OpenScreen.yml index 17aea373..3190c40d 100644 --- a/build/flatpak/com.getopenscreen.OpenScreen.yml +++ b/build/flatpak/com.getopenscreen.OpenScreen.yml @@ -23,6 +23,13 @@ separate-locales: false sdk-extensions: - org.freedesktop.Sdk.Extension.node22 - org.freedesktop.Sdk.Extension.rust-stable + # bindgen, reached through crates/compositor-view-napi and the pipewire helper, + # loads libclang at build time and the SDK does not ship it — run 31602275749 died + # exactly there. LLVM 18 is the freedesktop 24.08 base toolchain, so llvm18 is the + # extension that should match; stage A of the same workflow now enumerates every + # published Sdk.Extension for this runtime, so if this name is wrong the run says + # which one to use instead of costing another guess. + - org.freedesktop.Sdk.Extension.llvm18 command: openscreen finish-args: @@ -79,8 +86,12 @@ modules: - name: openscreen buildsystem: simple build-options: - append-path: /usr/lib/sdk/node22/bin:/usr/lib/sdk/rust-stable/bin + append-path: /usr/lib/sdk/node22/bin:/usr/lib/sdk/rust-stable/bin:/usr/lib/sdk/llvm18/bin env: + # bindgen resolves libclang through this, and build-linux-compositor-addon.mjs + # reads it too rather than searching. Pointing at the extension's lib dir is + # what makes the ffmpeg headers parseable at all. + LIBCLANG_PATH: /usr/lib/sdk/llvm18/lib # THE PATHS MATTER, and getting them wrong is why the first two spike runs # died in `npm ci`. flatpak-node-generator lays its output out relative to # the module build dir as `flatpak-node/`, so both of these are From 12b91e9f41ed7b740367bf72866d9f9007b666dd Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Wed, 12 Aug 2026 16:40:32 +0200 Subject: [PATCH 04/12] feat(flatpak): build the patchelf the compositor addon requires MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit llvm18 was the right name: run 31606798236 got bindgen through, and the pipewire helper compiled and ran its own probe — the "no ScreenCast portal" line is that probe reporting a CI runner has no desktop session, not a build failure. The wall moved to patchelf, which the SDK does not carry and build-linux-compositor-addon.mjs refuses to proceed without. That refusal is load-bearing rather than fussy: patchelf is what rewrites the ffmpeg symbol names so the addon cannot bind to Chromium's bundled libffmpeg.so, which is the same constraint that ruled out the ffmpeg-full extension and forced the from-source ffmpeg module. Built from the 0.18.0 release tarball, digest verified locally rather than copied from anywhere, and carrying `cleanup: ['*']` so a build tool does not end up inside the shipped image. --- .../flatpak/com.getopenscreen.OpenScreen.yml | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/build/flatpak/com.getopenscreen.OpenScreen.yml b/build/flatpak/com.getopenscreen.OpenScreen.yml index 3190c40d..b572d3b6 100644 --- a/build/flatpak/com.getopenscreen.OpenScreen.yml +++ b/build/flatpak/com.getopenscreen.OpenScreen.yml @@ -51,6 +51,23 @@ finish-args: - --filesystem=xdg-documents modules: + # --------------------------------------------------------------------------- + # 0. patchelf — a build tool, not a shipped dependency. + # + # build-linux-compositor-addon.mjs refuses to run without it, and for a reason + # central to this whole port: it is what rewrites the ffmpeg symbol names so the + # addon cannot bind to Chromium's bundled libffmpeg.so. The SDK does not carry it. + # + # `cleanup: ['*']` because later modules need it during the build and the finished + # app must not ship a copy of a build tool. + - name: patchelf + cleanup: + - '*' + sources: + - type: archive + url: https://github.com/NixOS/patchelf/releases/download/0.18.0/patchelf-0.18.0.tar.gz + sha256: "64de10e4c6b8b8379db7e87f58030f336ea747c0515f381132e810dbf84a86e7" + # --------------------------------------------------------------------------- # 1. ffmpeg, from source. # @@ -92,6 +109,9 @@ modules: # reads it too rather than searching. Pointing at the extension's lib dir is # what makes the ffmpeg headers parseable at all. LIBCLANG_PATH: /usr/lib/sdk/llvm18/lib + # Named rather than left to PATH: the script checks this variable first and + # module 0 installs to a known location, so there is nothing to discover. + PATCHELF: /app/bin/patchelf # THE PATHS MATTER, and getting them wrong is why the first two spike runs # died in `npm ci`. flatpak-node-generator lays its output out relative to # the module build dir as `flatpak-node/`, so both of these are From ae48282b285f7a5cf4665b55aadc7bfb2782102e Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Wed, 12 Aug 2026 21:52:49 +0200 Subject: [PATCH 05/12] feat(flatpak): pre-fetch the STT helper's three git dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit patchelf unblocked the compositor addon, and run 31608092415 then hit the before-pack guard: no whisper.cpp helper, no ggml shared objects. The guard is right to be a hard error — a payload missing them ships an app where transcription fails in front of the user — so the fix is to build the helper, not to skip it. electron/native/whisper-stt/CMakeLists.txt pulls whisper.cpp, cpp-httplib and nlohmann/json with FetchContent, which needs git and a network. Both are absent from a Flatpak sandbox, so all three are now pinned sources with tag AND commit, and CMake is redirected at the checkouts with FETCHCONTENT_SOURCE_DIR_*. whisper.cpp v1.9.1 carries ggml in-tree, so there is no fourth pin to keep in step. Those flags had nowhere to enter from. build-whisper-stt.sh rejects unrecognised CLI arguments on purpose, and CMake takes cache variables from the command line rather than the environment, so the script gains one seam: WHISPER_EXTRA_CMAKE_FLAGS, word-split into the existing BUILD_FLAGS. It is useful beyond Flatpak — any offline or vendored build needs the same redirection. Verified that it splits into separate flags and that an unset value does not trip `set -u` on either bash 3.2 or 5. --- .../flatpak/com.getopenscreen.OpenScreen.yml | 30 +++++++++++++++++++ scripts/build-whisper-stt.sh | 17 +++++++++++ 2 files changed, 47 insertions(+) diff --git a/build/flatpak/com.getopenscreen.OpenScreen.yml b/build/flatpak/com.getopenscreen.OpenScreen.yml index b572d3b6..235c911c 100644 --- a/build/flatpak/com.getopenscreen.OpenScreen.yml +++ b/build/flatpak/com.getopenscreen.OpenScreen.yml @@ -112,6 +112,14 @@ modules: # Named rather than left to PATH: the script checks this variable first and # module 0 installs to a known location, so there is nothing to discover. PATCHELF: /app/bin/patchelf + # electron/native/whisper-stt/CMakeLists.txt pulls three repositories with + # FetchContent, which wants git and a network. They are pinned as sources + # below and CMake is redirected at the checkouts. The names are FetchContent's + # own, uppercased: `whisper`, `httplib`, `json`. + WHISPER_EXTRA_CMAKE_FLAGS: >- + -DFETCHCONTENT_SOURCE_DIR_WHISPER=/run/build/openscreen/thirdparty/whisper.cpp + -DFETCHCONTENT_SOURCE_DIR_HTTPLIB=/run/build/openscreen/thirdparty/cpp-httplib + -DFETCHCONTENT_SOURCE_DIR_JSON=/run/build/openscreen/thirdparty/json # THE PATHS MATTER, and getting them wrong is why the first two spike runs # died in `npm ci`. flatpak-node-generator lays its output out relative to # the module build dir as `flatpak-node/`, so both of these are @@ -138,6 +146,10 @@ modules: - npm ci --offline - npm run build:native:linux - npm run build:native:compositor:linux + # Not optional, and the before-pack guard is right to refuse without it: run + # 31608092415 stopped here because a payload missing the STT helper ships an app + # where transcription fails in front of the user. + - npm run build:whisper-binaries - npm run build-vite # `dir`, because Flathub packages the app tree and has no use for a .deb or an # AppImage. executableName is pinned so the wrapper below can name the binary @@ -185,6 +197,24 @@ modules: # merge the lockfiles. - cargo-sources-crates.json - cargo-sources-pipewire.json + # The three FetchContent dependencies of the STT helper, pinned to tag AND + # commit because a tag alone is not reproducible. whisper.cpp v1.9.1 carries + # ggml in-tree, so there is no fourth pin to keep in step. + - type: git + url: https://github.com/ggml-org/whisper.cpp.git + tag: v1.9.1 + commit: f049fff95a089aa9969deb009cdd4892b3e74916 + dest: thirdparty/whisper.cpp + - type: git + url: https://github.com/yhirose/cpp-httplib.git + tag: v0.18.1 + commit: 5c1a34e766817cf0865ca8bc5af5edeb0f9b2df4 + dest: thirdparty/cpp-httplib + - type: git + url: https://github.com/nlohmann/json.git + tag: v3.11.3 + commit: 9cca280a4d0ccf0c08f47a99aa71d1b0e52f8d03 + dest: thirdparty/json # Zypak is mandatory, not a nicety: without it Electron's sandbox fails with # "SUID sandbox helper binary" and the app does not start. - type: script diff --git a/scripts/build-whisper-stt.sh b/scripts/build-whisper-stt.sh index 3dd1f304..e8894e96 100644 --- a/scripts/build-whisper-stt.sh +++ b/scripts/build-whisper-stt.sh @@ -314,6 +314,23 @@ BUILD_FLAGS=() if [[ -n "${DEFAULT_FLAG}" ]]; then BUILD_FLAGS+=("${DEFAULT_FLAG}") fi +# An escape hatch for builds that cannot reach the network. The CMakeLists pulls +# whisper.cpp, cpp-httplib and nlohmann/json with FetchContent, which needs git +# and a network; a Flatpak sandbox has neither, so the Flathub manifest pre-fetches +# all three as pinned sources and redirects CMake at them with +# -DFETCHCONTENT_SOURCE_DIR_. Those flags cannot arrive as CLI arguments — +# the parser above rejects anything it does not recognise, deliberately — and CMake +# reads cache variables from the command line rather than the environment, so this +# is the seam. +# +# Word-split on purpose: the value is a list of cmake flags, not one argument. +if [[ -n "${WHISPER_EXTRA_CMAKE_FLAGS:-}" ]]; then + # `read -a` rather than bare expansion so `set -u` and shellcheck both stay + # happy, and bash 3.2 on macOS handles it identically. + read -r -a EXTRA_CMAKE_FLAGS <<< "${WHISPER_EXTRA_CMAKE_FLAGS}" + BUILD_FLAGS+=("${EXTRA_CMAKE_FLAGS[@]}") + echo "[whisper-stt] extra cmake flags from WHISPER_EXTRA_CMAKE_FLAGS: ${WHISPER_EXTRA_CMAKE_FLAGS}" +fi # See the comment in build_variant() re: bash 3.2 + `set -u` + empty arrays # (macOS x64/CPU has no DEFAULT_FLAG, so BUILD_FLAGS is genuinely empty here). build_variant "default" ${BUILD_FLAGS[@]+"${BUILD_FLAGS[@]}"} From f09143c0dd8c85deb7c9c79eb019170073ad5882 Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Wed, 12 Aug 2026 22:09:02 +0200 Subject: [PATCH 06/12] feat(flatpak): supply the SPIRV-Headers ggml's Vulkan backend requires MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The FetchContent redirection works — run 31634845684 configured whisper.cpp and ggml out of the pinned local checkouts, so the three pins and the WHISPER_EXTRA_CMAKE_FLAGS seam do what they were added for. ggml's Vulkan backend then stopped the configure. It asks for exactly two things: Vulkan with glslc, which the runtime already satisfies at 1.3.290, and `find_package(SPIRV-Headers CONFIG REQUIRED)`, which nothing provides. Reading ggml-vulkan's CMakeLists rather than iterating showed those are the only two, so this should be the last dependency of that backend. Pinned to vulkan-sdk-1.3.290.0 to match the loader and glslc already in the runtime — "latest" would defeat the purpose of a package whose job is to agree with them. Build-only, so `cleanup: ['*']`. Forcing OSC_ENABLE_VULKAN=OFF was the cheaper alternative and is the wrong trade: it would leave Flatpak transcription running on CPU while the deb uses the GPU, over one missing headers package. --- .../flatpak/com.getopenscreen.OpenScreen.yml | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/build/flatpak/com.getopenscreen.OpenScreen.yml b/build/flatpak/com.getopenscreen.OpenScreen.yml index 235c911c..262c49b9 100644 --- a/build/flatpak/com.getopenscreen.OpenScreen.yml +++ b/build/flatpak/com.getopenscreen.OpenScreen.yml @@ -68,6 +68,31 @@ modules: url: https://github.com/NixOS/patchelf/releases/download/0.18.0/patchelf-0.18.0.tar.gz sha256: "64de10e4c6b8b8379db7e87f58030f336ea747c0515f381132e810dbf84a86e7" + # --------------------------------------------------------------------------- + # 0b. SPIRV-Headers — also build-only. + # + # ggml's Vulkan backend wants exactly two things (ggml/src/ggml-vulkan/ + # CMakeLists.txt): `find_package(Vulkan COMPONENTS glslc REQUIRED)`, which the + # runtime satisfies at 1.3.290 with glslc present, and + # `find_package(SPIRV-Headers CONFIG REQUIRED)`, which nothing provides. Only the + # second was missing in run 31634845684. + # + # Version matched to the runtime's Vulkan rather than "latest": the whole point of + # this package is agreeing with the loader and glslc that are already there. + # + # The alternative was building STT CPU-only by forcing OSC_ENABLE_VULKAN=OFF, which + # would have made Flatpak transcription slower than the deb's for no reason beyond + # one missing headers package. + - name: spirv-headers + buildsystem: cmake-ninja + cleanup: + - '*' + sources: + - type: git + url: https://github.com/KhronosGroup/SPIRV-Headers.git + tag: vulkan-sdk-1.3.290.0 + commit: 2acb319af38d43be3ea76bfabf3998e5281d8d12 + # --------------------------------------------------------------------------- # 1. ffmpeg, from source. # From 49417f97ff063a5dcfde874be574a9d8d556886a Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Wed, 12 Aug 2026 22:32:44 +0200 Subject: [PATCH 07/12] fix(flatpak): stage ffmpeg where the pipewire helper looks for it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SPIRV-Headers was the last thing ggml's Vulkan backend wanted: run 31636202957 built whisper.cpp, ggml and the Vulkan backend, then reached packaging — 21 minutes in, further than any run so far. The before-pack guard caught a gap the build had already passed over silently. FFMPEG_DIR points the compositor addon at /app, but build-linux-pipewire-helper.mjs reads `crates/thirdparty/ffmpeg-linux64-lgpl-shared/lib` directly to populate the helper's `helper-ffmpeg/`, and when that path is missing it warns instead of failing. So the helper shipped without its libraries and only the guard noticed, which is exactly the job that guard exists for. The two sets of libraries are not interchangeable, and the guard's own message says why: the helper needs the original sonames, while the addon's copies have every symbol renamed to `osff_*` so it cannot bind to Chromium's libffmpeg.so. Symlinking the vendored path at module 1's output satisfies both without a second ffmpeg in the build, and asserting `libav*.so*` resolves means a future layout change fails here rather than fifteen minutes later. --- build/flatpak/com.getopenscreen.OpenScreen.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/build/flatpak/com.getopenscreen.OpenScreen.yml b/build/flatpak/com.getopenscreen.OpenScreen.yml index 262c49b9..8cfea719 100644 --- a/build/flatpak/com.getopenscreen.OpenScreen.yml +++ b/build/flatpak/com.getopenscreen.OpenScreen.yml @@ -169,6 +169,23 @@ modules: # `fetch:ffmpeg:sdk`, which is a network download, and module 1 already # provides what it would have fetched. The remaining steps are spelled out. - npm ci --offline + # Present module 1's ffmpeg at the path the native scripts already look for. + # + # FFMPEG_DIR covers the compositor addon, but build-linux-pipewire-helper.mjs + # reads `crates/thirdparty/ffmpeg-linux64-lgpl-shared/lib` directly to populate + # the helper's own `helper-ffmpeg/` directory — and when that path is absent it + # WARNS rather than fails, so run 31636202957 sailed past it and was caught much + # later by the before-pack guard instead. The two copies are deliberately + # different: the helper needs the original sonames, while the addon gets every + # symbol renamed to `osff_*` so it cannot bind to Chromium's libffmpeg.so. + # + # Symlinks rather than a copy, so there is exactly one ffmpeg in the build. + - | + vendored=crates/thirdparty/ffmpeg-linux64-lgpl-shared + mkdir -p "$vendored" + ln -sfn /app/include "$vendored/include" + ln -sfn /app/lib "$vendored/lib" + ls "$vendored/lib"/libav*.so* >/dev/null - npm run build:native:linux - npm run build:native:compositor:linux # Not optional, and the before-pack guard is right to refuse without it: run From d148ad8206a64672dd5493743280f2e84c99765c Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Wed, 12 Aug 2026 22:56:56 +0200 Subject: [PATCH 08/12] fix(flatpak): waive the glibc floor for the one build it cannot apply to MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run 31638203167 staged the helper's ffmpeg correctly and reached the next guard: the symbol-version floor, which refuses anything needing a newer glibc than Ubuntu 22.04 provides. That guard is right about the deb, rpm, pacman and AppImage, and it is a category error here. A Flatpak does not resolve against the host's glibc — it runs inside org.freedesktop.Platform, whose glibc is newer than every distro the floor protects. The payload cannot have the problem the guard describes. The tempting fix is the wrong one: raising MAX_SYMBOL_VERSION would silently drop a distro from the packages that DO need the floor, which is the failure it was written for after 22.04, Debian 12 and RHEL 9 shipped with STT dying in ld.so. So the manifest sets OPENSCREEN_SYMBOL_FLOOR=runtime-provided, and that waives the ceiling comparison alone. The parser assertion still runs: whether the floor applies and whether the scan works at all are different questions, and the second is what keeps this guard from reporting "clean" forever. It logs loudly when waived, because a silent waiver is indistinguishable from a pass. --- .../flatpak/com.getopenscreen.OpenScreen.yml | 6 ++++ scripts/before-pack.cjs | 29 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/build/flatpak/com.getopenscreen.OpenScreen.yml b/build/flatpak/com.getopenscreen.OpenScreen.yml index 8cfea719..7967aef5 100644 --- a/build/flatpak/com.getopenscreen.OpenScreen.yml +++ b/build/flatpak/com.getopenscreen.OpenScreen.yml @@ -160,6 +160,12 @@ modules: # $CARGO_HOME, so CARGO_HOME has to be this exact path. CARGO_HOME: /run/build/openscreen/cargo CARGO_NET_OFFLINE: 'true' + # scripts/before-pack.cjs refuses payloads needing a newer glibc than Ubuntu + # 22.04 provides, which is right for the deb/rpm/pacman/AppImage and a category + # error here: a Flatpak resolves against org.freedesktop.Platform's glibc, not + # the host's. Raising MAX_SYMBOL_VERSION instead would drop a distro from the + # packages that genuinely need the floor. Waives the ceiling comparison only. + OPENSCREEN_SYMBOL_FLOOR: runtime-provided # crates/thirdparty/ffmpeg-linux64-lgpl-shared is what the native build # scripts look for and nothing provisions it here. Module 1 installed its # headers and libraries into /app, which is the shape they expect. diff --git a/scripts/before-pack.cjs b/scripts/before-pack.cjs index 227d2d48..807f838e 100644 --- a/scripts/before-pack.cjs +++ b/scripts/before-pack.cjs @@ -504,6 +504,23 @@ function checkLinuxNativePayload(context) { */ const MAX_SYMBOL_VERSION = { GLIBC: "2.35", GLIBCXX: "3.4.30", CXXABI: "1.3.13" }; +/** + * The one build where the ceiling above is a category error rather than a floor. + * + * A Flatpak does not link against the host's glibc: it runs inside its runtime, so + * `org.freedesktop.Platform` decides what the binaries resolve against, and that is + * newer than every distro the ceiling protects. Checking a Flatpak payload against + * Ubuntu 22.04's glibc refuses a build that cannot have the problem — and the only + * other way past it, raising MAX_SYMBOL_VERSION, would silently drop a distro from + * the deb/rpm/pacman/AppImage packages that DO need the floor. + * + * Set by build/flatpak/com.getopenscreen.OpenScreen.yml and nothing else. It waives + * ONLY the ceiling comparison: the parser assertion still runs, because "does the + * floor apply here" and "did the scan work at all" are unrelated questions, and the + * second is how this guard stays honest. + */ +const SYMBOL_FLOOR_WAIVED = process.env.OPENSCREEN_SYMBOL_FLOOR === "runtime-provided"; + /** Dotted numeric compare, so 3.4.9 < 3.4.30 and 2.4 < 2.38 rather than by string. */ function compareVersions(a, b) { const left = a.split(".").map(Number); @@ -618,6 +635,18 @@ function checkLinuxSymbolVersionFloor(dir) { ); } + // After the parser assertion on purpose — see SYMBOL_FLOOR_WAIVED. Loud, because a + // waived guard that says nothing is indistinguishable from a guard that passed. + if (SYMBOL_FLOOR_WAIVED) { + console.log( + `[before-pack] symbol-version floor WAIVED for ${scanned.length} ELF files in ` + + `${path.relative(ROOT, dir)}: OPENSCREEN_SYMBOL_FLOOR=runtime-provided.\n` + + " Valid only when the runtime ships its own glibc, which means the Flatpak build.\n" + + " Distro packages (deb/rpm/pacman/AppImage) must never set it.", + ); + return; + } + const offenders = scanned .map((entry) => ({ name: entry.name, From 29786d957dbfe3bcdf0916ea2f8c062a87e7182c Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Wed, 12 Aug 2026 23:21:56 +0200 Subject: [PATCH 09/12] fix(flatpak): stop electron-builder reaching for the network, and copy the right tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run 31640186488 cleared every guard — the waiver logged once, before-pack passed in full — and got as far as `packaging platform=linux arch=x64 electron=41.2.1` before dying on `getaddrinfo EAI_AGAIN github.com`. The generated sources do cache electron-v41.2.1-linux-x64.zip under flatpak-node/cache/electron, and XDG_CACHE_HOME points there, so this is a cache LAYOUT mismatch with this electron-builder rather than a missing artifact. Reverse-engineering the layout it wants is a guess; npm ci has already extracted the runtime into node_modules, so --config.electronDist points at that and there is nothing left to download. Asserted, so a missing dist fails with that sentence instead of a DNS error. Second bug, visible in the same line and unrelated to the network: appOutDir is release/1.9.2/linux-unpacked, because electron-builder.json5 sets directories.output to `release/${version}`. The copy read dist/, which has never existed here. Globbed and asserted rather than hardcoded, so a version bump does not silently install an empty /app/openscreen. --- .../flatpak/com.getopenscreen.OpenScreen.yml | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/build/flatpak/com.getopenscreen.OpenScreen.yml b/build/flatpak/com.getopenscreen.OpenScreen.yml index 7967aef5..28005581 100644 --- a/build/flatpak/com.getopenscreen.OpenScreen.yml +++ b/build/flatpak/com.getopenscreen.OpenScreen.yml @@ -202,8 +202,33 @@ modules: # `dir`, because Flathub packages the app tree and has no use for a .deb or an # AppImage. executableName is pinned so the wrapper below can name the binary # instead of discovering it. - - npx electron-builder --linux dir --config.npmRebuild=false --config.executableName=openscreen - - cp -r dist/linux-unpacked /app/openscreen + # electronDist rather than letting electron-builder resolve the runtime: run + # 31640186488 reached `packaging platform=linux arch=x64 electron=41.2.1` and then + # died on `getaddrinfo EAI_AGAIN github.com`. The generated sources DO cache + # electron-v41.2.1-linux-x64.zip under flatpak-node/cache/electron, but matching + # whatever cache layout this electron-builder expects is a guess, and npm ci has + # already extracted the runtime into node_modules. Point at that and no download + # can be attempted at all. + - | + test -d node_modules/electron/dist || { + echo "node_modules/electron/dist is missing — electron's postinstall did not extract the offline zip" >&2 + exit 1 + } + npx electron-builder --linux dir \ + --config.npmRebuild=false \ + --config.executableName=openscreen \ + --config.electronDist=node_modules/electron/dist + # NOT dist/: electron-builder.json5 sets `directories.output` to + # `release/${version}`, so the tree is release/1.9.2/linux-unpacked. Globbed + # rather than hardcoded so a version bump does not break this, and asserted so a + # miss fails here instead of installing an empty /app/openscreen. + - | + unpacked=$(echo release/*/linux-unpacked) + test -d "$unpacked" || { + echo "no linux-unpacked under release/ — looked for release/*/linux-unpacked" >&2 + exit 1 + } + cp -r "$unpacked" /app/openscreen - install -Dm755 openscreen.sh /app/bin/openscreen - install -Dm644 build/flatpak/com.getopenscreen.OpenScreen.desktop /app/share/applications/${FLATPAK_ID}.desktop # The metainfo's launchable is `openscreen.desktop`, which is what the deb From 297a2956063600cc1b5a0de888e8643e4448c1a9 Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Wed, 12 Aug 2026 23:50:48 +0200 Subject: [PATCH 10/12] ci(flatpak): check the gate Flathub actually applies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run 31642618732 built the Flatpak: zero errors, `Success!`, and the export named everything after the app id — the desktop file, all eight icon sizes, and the metainfo whose launchable the install-time rewrite fixed. Offline, from source, in twenty minutes. electronDist did its job too ("using custom unpacked Electron distribution"), so nothing reached for the network. "It builds" is not "they would take it", and the difference is flatpak-builder-lint, which is what Flathub runs at submission. Cheaper here than as a review round-trip. Both modes: `manifest` reads the file, `repo` reads what actually got exported and catches an icon or metainfo that silently did not — so stage F now also writes an OSTree repo for it to read. org.flatpak.Builder is installed for the lint alone. The build keeps apt's flatpak-builder, the one now proven to work, so this stays additive rather than swapping the toolchain under a green result. The verdict table no longer implies a green run means a working app. Nothing in CI has launched it, and a runner has no desktop session to launch it into. --- .github/workflows/flatpak-spike.yml | 47 ++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/.github/workflows/flatpak-spike.yml b/.github/workflows/flatpak-spike.yml index b7a999c2..03cf7de8 100644 --- a/.github/workflows/flatpak-spike.yml +++ b/.github/workflows/flatpak-spike.yml @@ -184,11 +184,47 @@ jobs: continue-on-error: true run: | set -euo pipefail + # --repo exports an OSTree repo alongside the build tree, which is what + # stage G needs: the repo lint catches things the manifest lint cannot see, + # like an icon or metainfo that did not actually get exported. flatpak-builder --user --install-deps-from=flathub --force-clean \ --disable-rofiles-fuse \ + --repo=/tmp/flatpak-repo \ /tmp/flatpak-build build/flatpak/com.getopenscreen.OpenScreen.yml \ 2>&1 | tee /tmp/flatpak-build.log + # --------------------------------------------------------------------- + # Stage G: the gate Flathub actually applies at submission time. Running it + # here is the difference between "it builds" and "they would accept it", and + # it costs one flatpak install rather than a review round-trip. + # + # org.flatpak.Builder only for the lint — the build above keeps apt's + # flatpak-builder, which is the one proven to work, so this stays additive. + - name: "Stage G — flatpak-builder-lint" + id: lint + continue-on-error: true + run: | + set -euo pipefail + flatpak install -y --user --noninteractive flathub org.flatpak.Builder + lint() { + local mode="$1" target="$2" + echo "### flatpak-builder-lint $mode" >> "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + if flatpak run --command=flatpak-builder-lint org.flatpak.Builder \ + "$mode" "$target" 2>&1 | tee "/tmp/lint-$mode.json"; then + echo "clean" >> "$GITHUB_STEP_SUMMARY" + else + cat "/tmp/lint-$mode.json" >> "$GITHUB_STEP_SUMMARY" + fi + echo '```' >> "$GITHUB_STEP_SUMMARY" + } + # Both, and neither is allowed to abort the other: they report different + # classes of problem and a submission has to satisfy the two. + rc=0 + lint manifest build/flatpak/com.getopenscreen.OpenScreen.yml || rc=1 + lint repo /tmp/flatpak-repo || rc=1 + exit $rc + - name: Collect artifacts if: always() uses: actions/upload-artifact@v7 @@ -210,6 +246,7 @@ jobs: C: ${{ steps.nodegen.outcome }} D: ${{ steps.vendor.outcome }} F: ${{ steps.build.outcome }} + G: ${{ steps.lint.outcome }} NPM_COUNT: ${{ steps.nodegen.outputs.count }} run: | { @@ -222,10 +259,10 @@ jobs: echo "| C — offline npm sources (${NPM_COUNT:-n/a} entries) | $C |" echo "| D — cargo sources, both lockfiles | $D |" echo "| F — flatpak-builder | $F |" + echo "| G — flatpak-builder-lint | $G |" echo - echo "A–D green means the offline inputs are all wired. F is now the whole" - echo "question: ffmpeg is known to compile, so a failure past it is about the" - echo "app module — the npm cache paths, the shared cargo vendor directory, or" - echo "the Electron packaging. Read the tail of the build log in the" - echo "\`flatpak-spike\` artifact; a green F still does not mean the app RUNS." + echo "F green means it builds offline from source. G green means Flathub would" + echo "not reject it on sight. **Neither means the app runs** — nothing here has" + echo "launched it, and a CI runner has no desktop session or ScreenCast portal to" + echo "launch it into. That verification needs a Linux desktop and a human." } >> "$GITHUB_STEP_SUMMARY" From b16a64b3be6a2badab3463f397f0f058c3353f59 Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Thu, 13 Aug 2026 00:16:29 +0200 Subject: [PATCH 11/12] fix(flatpak): export the repo where the sandboxed linter can read it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The manifest lint came back clean on run 31644382437 — no errors, one warning that org.freedesktop.Platform 25.08 exists and one info line about portal talk-name access. Flathub would not reject the manifest. The repo lint died on `Could not find repo directory: /tmp/flatpak-repo`, and flatpak-builder had exported it correctly ("Exporting com.getopenscreen.OpenScreen to repo"). A flatpak always gets a private /tmp regardless of the filesystem permissions it holds, so the linter inside org.flatpak.Builder was looking at its own empty /tmp. The manifest lint read a workspace-relative path in the same invocation without trouble, which is what identifies the fix: export to $GITHUB_WORKSPACE instead. Leaving the 25.08 warning alone. Moving runtimes means re-establishing whether node22, llvm18 and the Electron BaseApp all publish for it, which is a fresh set of the questions stages A and B exist to answer — and 24.08 is a warning, not a rejection. --- .github/workflows/flatpak-spike.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/flatpak-spike.yml b/.github/workflows/flatpak-spike.yml index 03cf7de8..52a41bf4 100644 --- a/.github/workflows/flatpak-spike.yml +++ b/.github/workflows/flatpak-spike.yml @@ -187,9 +187,14 @@ jobs: # --repo exports an OSTree repo alongside the build tree, which is what # stage G needs: the repo lint catches things the manifest lint cannot see, # like an icon or metainfo that did not actually get exported. + # + # In the workspace and NOT in /tmp. A flatpak always gets a private /tmp, + # whatever filesystem permissions it holds, so the sandboxed linter in stage G + # cannot see a host /tmp path — it read the manifest by a workspace-relative + # path perfectly well, which is what makes this the fix. flatpak-builder --user --install-deps-from=flathub --force-clean \ --disable-rofiles-fuse \ - --repo=/tmp/flatpak-repo \ + --repo="$GITHUB_WORKSPACE/flatpak-repo" \ /tmp/flatpak-build build/flatpak/com.getopenscreen.OpenScreen.yml \ 2>&1 | tee /tmp/flatpak-build.log @@ -222,7 +227,7 @@ jobs: # classes of problem and a submission has to satisfy the two. rc=0 lint manifest build/flatpak/com.getopenscreen.OpenScreen.yml || rc=1 - lint repo /tmp/flatpak-repo || rc=1 + lint repo "$GITHUB_WORKSPACE/flatpak-repo" || rc=1 exit $rc - name: Collect artifacts From b8d80c34ede48dbe12c98301f935130f73373427 Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Thu, 13 Aug 2026 00:42:29 +0200 Subject: [PATCH 12/12] fix(flatpak): stop requesting portal bus names the sandbox already has MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both lints ran on run 31646262387, and the manifest one was not clean after all — I had read the tail of its JSON, which starts at "warnings", and missed the "errors" array above it. The real error is mine: finish-args-portal-talk-name. Every sandbox can reach the XDG portal bus names without asking, so `--talk-name=org.freedesktop.portal.*` is redundant, and flatpak-builder-lint treats anything with that prefix as an error rather than a warning (checks/finish_args.py). Screen capture keeps working — the helper speaks org.freedesktop.portal.ScreenCast through the same implicit access. The repo lint's other two, appstream-external-screenshot-url and appstream-screenshots-not-mirrored-in-ostree, are not in our files: the check wants a `screenshots/` OSTree ref that only Flathub's mirroring step creates, and it skips itself when a test ref is present, which is how their CI avoids it. A locally built repo has neither, so those two are the one class of finding this spike cannot settle from here. --- build/flatpak/com.getopenscreen.OpenScreen.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/build/flatpak/com.getopenscreen.OpenScreen.yml b/build/flatpak/com.getopenscreen.OpenScreen.yml index 28005581..cef027cd 100644 --- a/build/flatpak/com.getopenscreen.OpenScreen.yml +++ b/build/flatpak/com.getopenscreen.OpenScreen.yml @@ -37,11 +37,13 @@ finish-args: - --socket=wayland - --socket=fallback-x11 - --socket=pulseaudio - # Screen capture goes through the portal rather than a raw socket, which is the - # one part of this port that was already sandbox-shaped: the helper in - # electron/native/pipewire-capture speaks org.freedesktop.portal.ScreenCast. - - --talk-name=org.freedesktop.portal.ScreenCast - - --talk-name=org.freedesktop.portal.Desktop + # NO --talk-name for the portals, deliberately. Screen capture goes through + # org.freedesktop.portal.ScreenCast — the helper in + # electron/native/pipewire-capture speaks it, which is the one part of this port + # that was already sandbox-shaped — but portal bus names are reachable from every + # sandbox without being requested, so asking is both redundant and rejected: + # flatpak-builder-lint raises finish-args-portal-talk-name for anything starting + # with `org.freedesktop.portal.`, and it is an error, not a warning. # Webcam. - --device=all # The compositor addon is a Vulkan renderer.