diff --git a/.github/workflows/flatpak-spike.yml b/.github/workflows/flatpak-spike.yml
index 26a50976..52a41bf4 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:
@@ -126,51 +141,95 @@ 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
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.
+ #
+ # 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="$GITHUB_WORKSPACE/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 "$GITHUB_WORKSPACE/flatpak-repo" || rc=1
+ exit $rc
+
- name: Collect artifacts
if: always()
uses: actions/upload-artifact@v7
@@ -179,6 +238,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 +250,9 @@ jobs:
B: ${{ steps.install.outcome }}
C: ${{ steps.nodegen.outcome }}
D: ${{ steps.vendor.outcome }}
- E: ${{ steps.ffmpeg.outcome }}
F: ${{ steps.build.outcome }}
+ G: ${{ steps.lint.outcome }}
NPM_COUNT: ${{ steps.nodegen.outputs.count }}
- FFMPEG_SHA: ${{ steps.ffmpeg.outputs.sha }}
run: |
{
echo "## Verdict"
@@ -202,12 +262,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 "| G — flatpak-builder-lint | $G |"
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 "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"
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 e4d04af3..cef027cd 100644
--- a/build/flatpak/com.getopenscreen.OpenScreen.yml
+++ b/build/flatpak/com.getopenscreen.OpenScreen.yml
@@ -1,30 +1,35 @@
-# 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
+ # 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:
@@ -32,47 +37,76 @@ 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.
- - --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
- # 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.
+ # 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"
+
+ # ---------------------------------------------------------------------------
+ # 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.
#
- # 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.
+ # 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.
#
- # 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,40 +120,180 @@ 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 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:/usr/lib/sdk/llvm18/bin
env:
- # Point the native build at the ffmpeg this manifest just built rather than
- # at crates/thirdparty/, which nothing provisions inside the sandbox.
+ # 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
+ # 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
+ # /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'
+ # 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.
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
+ # 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
+ # 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
- # 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
+ # `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.
+ # 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
+ # 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
+ # 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
+ dest-filename: openscreen.sh
+ commands:
+ - export TMPDIR="$XDG_RUNTIME_DIR/app/$FLATPAK_ID"
+ - exec zypak-wrapper /app/openscreen/openscreen "$@"
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,
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[@]}"}