Skip to content

fix(linux): declare the libraries the packages need, and prove it on a clean machine - #325

Merged
EtienneLescot merged 6 commits into
mainfrom
claude/test-1-9-1-packaging-linux-da245e
Aug 10, 2026
Merged

fix(linux): declare the libraries the packages need, and prove it on a clean machine#325
EtienneLescot merged 6 commits into
mainfrom
claude/test-1-9-1-packaging-linux-da245e

Conversation

@EtienneLescot

@EtienneLescot EtienneLescot commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

1.9.1 fixed the symbol-version floor and shipped three sonames that nothing
declared and nothing bundled. Measured on the published .deb, installed into a
bare ubuntu:22.04:

soname consumers consequence when absent
libgbm.so.1 the Electron binary itself the app does not start — exit 127
libasound.so.2 same binary same
libgomp.so.1 all 32 ELFs of the STT stack app starts, transcription dies in ld.so

before-pack.cjs could not have caught any of it: it checks how new a required
symbol is, not whether the library carrying it is ever installed. And nothing
else was watching — the depends lists are hand-written, and electron-builder
passes fpm none of --rpm-autoreq*.

All three hid behind the same accident. Desktop metapackages pull every one, and
libgomp1 only via libfftw3-single3, libimagequant0 and libsoxr0 — three
peripheral media libraries. Every machine anyone tested on had them.

A second, unrelated defect surfaced while verifying the Arch package: it is
uninstallable. http-parser is part of electron-builder's stale default
pacman list, Arch dropped the package, nothing Provides it, and pacman refuses
the whole transaction. Nothing in our payload wants it either.

The part that generalises

scripts/verify-linux-package.sh installs a built package into a bare container
of the target distro, runs ldd over every shipped ELF, and reports any soname
the package neither declares nor bundles — then starts the three executables,
because reaching main() is what ldd cannot show. Same answer Windows reached
one release earlier with verify-appx-native.ps1, and the reasoning is the same:
a static check only ever knows about the mistakes already made.

It runs on all three formats before the upload step, so a package that cannot
resolve blocks its own artifact and the release. rpm and pacman had no other
safety net at all — the pacman run is what surfaced http-parser on its first
execution.

The AppImage is deliberately not covered: it has no dependency mechanism, so
there is no declaration to verify against.

Discord announcement

Moved onto release: published, the one event every release path shares. As a
step of prerelease.yml/promote.yml it could only announce what those workflows
did, so a release cut by pushing a tag straight at build.yml — which is how 1.9.2
ships — would have gone out silent. It also fixes an RC bug that was invisible
there: prerelease.yml announced right after dispatching build.yml, so
#rc-testing was pointed at artifacts that would not exist for another twenty
minutes.

Verification

  • Against the published 1.9.1 .deb: fails with exactly the three sonames, no false positives.
  • Against the same package with them declared: PASS, 39 ELFs resolving, all three executables reaching main().
  • Arch: published 1.9.1 refuses to install; with http-parser neutralised it installs and all 39 ELFs resolve.
  • Fedora: passed already — its base image and gtk3's closure supply all three. Declaring them makes that true rather than lucky.
  • End to end on a real Ubuntu 24.04 desktop with mesa-vulkan-drivers removed: apt install pulls it back as declared, and the compositor preview renders.

No application code changes — nothing under electron/, src/ or crates/. The
payload is byte-identical to 1.9.1.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Linux packages now include required graphics, audio, Vulkan, and runtime dependencies for improved compatibility.
    • Release announcements are automatically posted when a release is published, with manual recovery still supported.
  • Bug Fixes

    • Added clean-environment validation to detect missing Linux package dependencies before release.
    • Removed duplicate prerelease and stable-release announcements.
  • Documentation

    • Added guidance for Linux package verification and release testing.

…sked for

The same shape as the glibc ceiling this release already fixed, and invisible to
the guard that fixed it. before-pack.cjs checks how NEW a required symbol is; it
has nothing to say about whether the library carrying it is ever installed. So
1.9.1 shipped three sonames that nothing declared and nothing bundled.

Measured on the published 1.9.1 .deb, installed into a bare ubuntu:22.04:

  libgbm.so.1     the Electron binary itself. The app exits 127 on
                  "error while loading shared libraries" before any window.
  libasound.so.2  same binary, same result.
  libgomp.so.1    GCC's OpenMP runtime, needed by all 32 ELFs of the STT stack
                  (whisper-stt-server, the libggml*, libwhisper, libparakeet).
                  The app starts and only transcription dies in ld.so.

None is in electron-builder's default deb list, which never followed Chromium to
GBM, and `depends` REPLACES that default rather than extending it, so there was
no layer left to catch them.

All three hid behind the same accident: desktop metapackages pull every one, and
libgomp1 only via libfftw3-single3, libimagequant0 and libsoxr0 — three
peripheral media libraries no desktop needs. Every machine anyone tested on had
them, including this one, which is why a package that cannot start on a clean
distro read as fine for two releases. A minimal install or a hand-assembled WM
has none of those guarantees, and the AppImage has no way to declare any of it.

Names resolved per distro rather than guessed, each in a container of that
distro. It matters: libgomp.so.1 is `libgomp1` on Debian but `libgomp` on Fedora
AND on Arch, where it was split out of `gcc-libs` — the obvious guess is wrong.
`libasound2` is right for both Ubuntu 22.04 and 24.04, where the real package is
`libasound2t64` and Provides the short name, exactly as `libgtk-3-0` already
relies on.

Fedora turns out not to have been broken: its base image plus gtk3's closure
already supply all three, and the rpm passes without this change. Declaring them
is what makes that true rather than lucky.
The .pacman artifact of 1.9.1 cannot be installed at all. Not degraded, not
missing a feature — pacman refuses the whole transaction:

    warning: cannot resolve "http-parser", a dependency of "openscreen"
    error: failed to prepare transaction (could not satisfy dependencies)
    :: unable to satisfy dependency 'http-parser' required by openscreen

`http-parser` is part of electron-builder's default pacman `depends`, written
when Node still used it. Node moved to llhttp years ago and Arch dropped the
package; nothing in the repos Provides it today. Nothing in our payload wants it
either — it appears in none of the 49 DT_NEEDED sonames the package carries.

This is the one entry in this file that deliberately departs from the
electron-builder default, against the rule the deb block states, so it gets said
out loud in a comment: removing a default is normally how a package breaks
silently, and this is the case where keeping one is.

`libappindicator-gtk3`, immediately below, is stale in exactly the same way and
was left alone on purpose — `libappindicator` declares it in both Provides and
Replaces, so it still resolves. Checked, not assumed.

Verified on archlinux:latest by installing the published 1.9.1 package with
--assume-installed http-parser, which is precisely what removing the line does:
the install succeeds, all 39 shipped ELFs resolve, and the main binary reaches
main() instead of dying in ld.so.

Found by the verification job that follows this commit, on its first run.
…lled

Three Linux releases in a row shipped a dependency the target machine could not
resolve, and each fix arrived with a guard aimed at the failure already
understood. The colocation check would never have caught the glibc floor; the
symbol-version check would never have caught a library that is simply not
installed anywhere. Both are worth keeping and neither generalises, which is the
actual problem — a static check only ever knows about the mistakes already made.
Windows reached this conclusion one release earlier (verify-appx-native.ps1);
this is the same answer for Linux.

So ask the loader. verify-linux-package.sh installs a built package into a BARE
container of the target distro, runs ldd over every ELF that ships, and reports
any soname the package neither declares nor bundles. Then it starts openscreen,
whisper-stt-server and openscreen-pipewire-helper, because reaching main() is the
part ldd cannot show: a binary the loader rejects exits 127 with "error while
loading shared libraries", while one that starts prints its own usage or its own
structured error.

The container is the point rather than an implementation detail. Every failure of
this shape has one cause — the machines we build on have more installed than the
machines we ship to — so a check running on the runner is not a check. For the
same reason the probe installs no tooling inside the container: binutils would
arrive with a transitive closure that could mask what is being measured, and ldd
is glibc, already there.

Two things it gets right that a first attempt did not. Libraries are judged by
whether they ship, not by RUNPATH: the bundled ffmpeg objects carry no RUNPATH of
their own and reach libavutil only through the binary that loads them, so ldd run
against one of THEM reports three sonames missing that are present and fine. And
it asserts it found at least ten ELFs, because a probe that silently stops looking
reports clean forever after.

rpm and pacman are covered too, and they are the ones with no other safety net:
nobody installs them often enough to report a gap quickly. That paid for itself
immediately — the pacman run is what surfaced http-parser, an entry that made the
Arch package uninstallable and that no amount of reading the list would have
revealed.

Verified both ways before wiring it up. Against the published 1.9.1 .deb it fails
with exactly libgbm.so.1, libasound.so.2 and libgomp.so.1 and no false positives;
against the same package repacked with those three declared it passes, 39 ELFs
resolving and all three executables reaching main(). The negative case is the one
that matters here.

The AppImage is deliberately not covered. It has no dependency mechanism, so
there is no declaration to verify against and every system soname is missing by
construction. It stays exposed — that is what d3d_linux::diagnose naming the Mesa
package is for.

The job runs before the upload step, and publish-release needs build-linux, so a
package that cannot resolve blocks both its own artifact and the release.
…at made it

The Discord announcement was a step of prerelease.yml and promote.yml, so it
could only ever announce what those two workflows did. A release cut by pushing a
tag straight at build.yml — which is how 1.9.2 ships — runs neither, and would
have gone out silent. That is the same class of miss the standalone dispatch was
extracted for after v1.9.0, one level up: the recovery path was fixed and the
trigger was not.

Move it onto `release: published`, the one event every path shares, and read
`github.event.release.prerelease` for the stable/rc split that used to be
hand-passed. This works only because build.yml creates releases with
OPENSCREEN_RELEASE_TOKEN — a release created with the default GITHUB_TOKEN
triggers no workflows, and this change would silently do nothing.

It also fixes a bug in the RC path that was invisible while it lived there:
prerelease.yml announced immediately after DISPATCHING build.yml rather than
after it completed, so #rc-testing was told to go test a build whose artifacts
would not exist for another twenty minutes. `release: published` means, by
definition, that the assets are attached.

Both call sites are removed rather than left as a fallback, because two triggers
on one event post twice. Nothing is lost with them: promote.yml passed RC_TAG to
get a "Promoted from <rc>" line the script only ever renders under `isRc &&
rcTag`, and it passed KIND: stable, so that line has never appeared in a stable
announcement. `release_notes_extra` no longer reaches Discord automatically —
prepending something stays possible through the dispatch inputs, which are kept.

STRICT is now "1" on every path. It was "0" inside the ceremonies for a real
reason — the release was already out and a failed announcement had to not report
it as broken — and standing alone inverts that: nothing depends on this job, so a
red run costs a notification while a green run that posted nothing recreates the
exact failure this exists to prevent.
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6cad90a8-6b6f-47f1-8585-567cf606bcea

📥 Commits

Reviewing files that changed from the base of the PR and between 378810e and e61b468.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (9)
  • .github/workflows/announce-release.yml
  • .github/workflows/build.yml
  • .github/workflows/prerelease.yml
  • .github/workflows/promote.yml
  • electron-builder.json5
  • nix/package.nix
  • package.json
  • scripts/verify-linux-package.sh
  • technical-documentation/engineering/build-and-packaging.md

📝 Walkthrough

Walkthrough

The release workflows now announce published releases through one workflow. Linux packages declare additional runtime dependencies and undergo clean-container verification for ELF resolution and executable startup.

Changes

Release announcement flow

Layer / File(s) Summary
Resolve published release context
.github/workflows/announce-release.yml
The workflow handles published-release events and manual dispatches. It resolves the release tag and stable or RC kind before selecting announcement settings.
Route workflow announcements
.github/workflows/prerelease.yml, .github/workflows/promote.yml, package.json
Prerelease and promotion workflows remove direct Discord announcements. The package version changes to 1.9.2.
Linux package verification
Layer / File(s) Summary
Declare Linux runtime dependencies
electron-builder.json5, nix/package.nix
Debian, Arch, and RPM packages add Vulkan, GBM, ALSA, and OpenMP runtime dependencies. The Nix dependency hash is updated.
Verify packages in clean containers
scripts/verify-linux-package.sh
The verifier installs Debian, RPM, and pacman packages in matching containers, checks package layout, scans ELF dependencies, and probes native executables.
Integrate and document verification
.github/workflows/build.yml, technical-documentation/engineering/build-and-packaging.md
The Linux build invokes verification for Debian, RPM, and pacman artifacts. Documentation describes the checks and excludes AppImage.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ReleaseEvent
  participant announce-release.yml
  participant Discord
  ReleaseEvent->>announce-release.yml: publish release
  announce-release.yml->>announce-release.yml: resolve tag and release kind
  announce-release.yml->>Discord: post release announcement
Loading
sequenceDiagram
  participant build.yml
  participant verify-linux-package.sh
  participant CleanContainer
  participant LinuxPackage
  build.yml->>verify-linux-package.sh: verify package artifact
  verify-linux-package.sh->>CleanContainer: mount artifact and select distro image
  CleanContainer->>LinuxPackage: install package
  CleanContainer->>CleanContainer: scan ELF dependencies
  CleanContainer->>CleanContainer: start native executables
  CleanContainer-->>build.yml: pass or fail verification
Loading

Possibly related PRs

Suggested reviewers: claude

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/test-1-9-1-packaging-linux-da245e

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Carried in this PR rather than as a separate commit on main, because 1.9.2 does
not go through prerelease.yml: there is no RC to promote, so nothing else would
write the version, and build.yml's publish guard rejects a tag whose version does
not match package.json.

Both manifests, via set-release-version.mjs — a sed over package.json alone is
what shipped three releases with a lockfile disagreeing with the package it
locks.
nix-check.yml only runs on PRs touching package-lock.json or nix/**, so it fires
here on a two-line version bump and reports a mismatch this branch did not cause.
The recorded hash was pinned to a dependency set that main's lockfile stopped
resolving to some time ago — exactly the drift that workflow was added to surface,
now surfaced by the first PR to touch the lockfile since.

The bump changes two lines, both the `version` field, and no dependency at all,
so nothing here alters what npm resolves. Setting it to what
`prefetch-npm-deps package-lock.json` reports makes `nix build` work again.

That bump also repairs a related inconsistency: main's package-lock.json still
said 1.8.0 while package.json said 1.9.0, which is the exact failure
set-release-version.mjs exists to prevent and which predates it.
@EtienneLescot
EtienneLescot merged commit 5e8b9d2 into main Aug 10, 2026
15 of 16 checks passed
@EtienneLescot
EtienneLescot deleted the claude/test-1-9-1-packaging-linux-da245e branch August 10, 2026 10:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant